class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self._version = __version__ self.setWindowIcon(QIcon(":/logo.png")) self.setWindowTitle("Tasmota Device Manager {}".format(self._version)) self.unknown = [] self.env = TasmotaEnvironment() self.device = None self.topics = [] self.mqtt_queue = [] self.fulltopic_queue = [] # ensure TDM directory exists in the user directory if not os.path.isdir("{}/TDM".format(QDir.homePath())): os.mkdir("{}/TDM".format(QDir.homePath())) self.settings = QSettings("{}/TDM/tdm.cfg".format(QDir.homePath()), QSettings.IniFormat) self.devices = QSettings("{}/TDM/devices.cfg".format(QDir.homePath()), QSettings.IniFormat) self.setMinimumSize(QSize(1000, 600)) # configure logging logging.basicConfig(filename="{}/TDM/tdm.log".format(QDir.homePath()), level=self.settings.value("loglevel", "INFO"), datefmt="%Y-%m-%d %H:%M:%S", format='%(asctime)s [%(levelname)s] %(message)s') logging.info("### TDM START ###") # load devices from the devices file, create TasmotaDevices and add the to the envvironment for mac in self.devices.childGroups(): self.devices.beginGroup(mac) device = TasmotaDevice(self.devices.value("topic"), self.devices.value("full_topic"), self.devices.value("friendly_name")) device.debug = self.devices.value("debug", False, bool) device.p['Mac'] = mac.replace("-", ":") device.env = self.env self.env.devices.append(device) # load device command history self.devices.beginGroup("history") for k in self.devices.childKeys(): device.history.append(self.devices.value(k)) self.devices.endGroup() self.devices.endGroup() self.device_model = TasmotaDevicesModel(self.env) self.setup_mqtt() self.setup_main_layout() self.add_devices_tab() self.build_mainmenu() # self.build_toolbars() self.setStatusBar(QStatusBar()) pbSubs = QPushButton("Show subscriptions") pbSubs.setFlat(True) pbSubs.clicked.connect(self.showSubs) self.statusBar().addPermanentWidget(pbSubs) self.queue_timer = QTimer() self.queue_timer.timeout.connect(self.mqtt_publish_queue) self.queue_timer.start(250) self.auto_timer = QTimer() self.auto_timer.timeout.connect(self.auto_telemetry) self.load_window_state() if self.settings.value("connect_on_startup", False, bool): self.actToggleConnect.trigger() self.tele_docks = {} self.consoles = [] def setup_main_layout(self): self.mdi = QMdiArea() self.mdi.setActivationOrder(QMdiArea.ActivationHistoryOrder) self.mdi.setTabsClosable(True) self.setCentralWidget(self.mdi) def setup_mqtt(self): self.mqtt = MqttClient() self.mqtt.connecting.connect(self.mqtt_connecting) self.mqtt.connected.connect(self.mqtt_connected) self.mqtt.disconnected.connect(self.mqtt_disconnected) self.mqtt.connectError.connect(self.mqtt_connectError) self.mqtt.messageSignal.connect(self.mqtt_message) def add_devices_tab(self): self.devices_list = ListWidget(self) sub = self.mdi.addSubWindow(self.devices_list) sub.setWindowState(Qt.WindowMaximized) self.devices_list.deviceSelected.connect(self.selectDevice) self.devices_list.openConsole.connect(self.openConsole) self.devices_list.openRulesEditor.connect(self.openRulesEditor) self.devices_list.openTelemetry.connect(self.openTelemetry) self.devices_list.openWebUI.connect(self.openWebUI) def load_window_state(self): wndGeometry = self.settings.value('window_geometry') if wndGeometry: self.restoreGeometry(wndGeometry) def build_mainmenu(self): mMQTT = self.menuBar().addMenu("MQTT") self.actToggleConnect = QAction(QIcon(":/disconnect.png"), "Connect") self.actToggleConnect.setCheckable(True) self.actToggleConnect.toggled.connect(self.toggle_connect) mMQTT.addAction(self.actToggleConnect) mMQTT.addAction(QIcon(), "Broker", self.setup_broker) mMQTT.addAction(QIcon(), "Autodiscovery patterns", self.patterns) mMQTT.addSeparator() mMQTT.addAction(QIcon(), "Clear obsolete retained LWTs", self.clear_LWT) mMQTT.addSeparator() mMQTT.addAction(QIcon(), "Auto telemetry period", self.auto_telemetry_period) self.actToggleAutoUpdate = QAction(QIcon(":/auto_telemetry.png"), "Auto telemetry") self.actToggleAutoUpdate.setCheckable(True) self.actToggleAutoUpdate.toggled.connect(self.toggle_autoupdate) mMQTT.addAction(self.actToggleAutoUpdate) mSettings = self.menuBar().addMenu("Settings") mSettings.addAction(QIcon(), "BSSId aliases", self.bssid) mSettings.addSeparator() mSettings.addAction(QIcon(), "Preferences", self.prefs) # mExport = self.menuBar().addMenu("Export") # mExport.addAction(QIcon(), "OpenHAB", self.openhab) def build_toolbars(self): main_toolbar = Toolbar(orientation=Qt.Horizontal, iconsize=24, label_position=Qt.ToolButtonTextBesideIcon) main_toolbar.setObjectName("main_toolbar") def initial_query(self, device, queued=False): for c in initial_commands(): cmd, payload = c cmd = device.cmnd_topic(cmd) if queued: self.mqtt_queue.append([cmd, payload]) else: self.mqtt.publish(cmd, payload, 1) def setup_broker(self): brokers_dlg = BrokerDialog() if brokers_dlg.exec_( ) == QDialog.Accepted and self.mqtt.state == self.mqtt.Connected: self.mqtt.disconnect() def toggle_autoupdate(self, state): if state == True: if self.mqtt.state == self.mqtt.Connected: for d in self.env.devices: self.mqtt.publish(d.cmnd_topic('STATUS'), payload=8) self.auto_timer.setInterval( self.settings.value("autotelemetry", 5000, int)) self.auto_timer.start() else: self.auto_timer.stop() def toggle_connect(self, state): if state and self.mqtt.state == self.mqtt.Disconnected: self.broker_hostname = self.settings.value('hostname', 'localhost') self.broker_port = self.settings.value('port', 1883, int) self.broker_username = self.settings.value('username') self.broker_password = self.settings.value('password') self.mqtt.hostname = self.broker_hostname self.mqtt.port = self.broker_port if self.broker_username: self.mqtt.setAuth(self.broker_username, self.broker_password) self.mqtt.connectToHost() elif not state and self.mqtt.state == self.mqtt.Connected: self.mqtt_disconnect() def auto_telemetry(self): if self.mqtt.state == self.mqtt.Connected: for d in self.env.devices: self.mqtt.publish(d.cmnd_topic('STATUS'), payload=8) def mqtt_connect(self): self.broker_hostname = self.settings.value('hostname', 'localhost') self.broker_port = self.settings.value('port', 1883, int) self.broker_username = self.settings.value('username') self.broker_password = self.settings.value('password') self.mqtt.hostname = self.broker_hostname self.mqtt.port = self.broker_port if self.broker_username: self.mqtt.setAuth(self.broker_username, self.broker_password) if self.mqtt.state == self.mqtt.Disconnected: self.mqtt.connectToHost() def mqtt_disconnect(self): self.mqtt.disconnectFromHost() def mqtt_connecting(self): self.statusBar().showMessage("Connecting to broker") def mqtt_connected(self): self.actToggleConnect.setIcon(QIcon(":/connect.png")) self.actToggleConnect.setText("Disconnect") self.statusBar().showMessage("Connected to {}:{} as {}".format( self.broker_hostname, self.broker_port, self.broker_username if self.broker_username else '[anonymous]')) self.mqtt_subscribe() def mqtt_subscribe(self): # clear old topics self.topics.clear() custom_patterns.clear() # load custom autodiscovery patterns self.settings.beginGroup("Patterns") for k in self.settings.childKeys(): custom_patterns.append(self.settings.value(k)) self.settings.endGroup() # expand fulltopic patterns to subscribable topics for pat in default_patterns: # tasmota default and SO19 self.topics += expand_fulltopic(pat) # check if custom patterns can be matched by default patterns for pat in custom_patterns: if pat.startswith("%prefix%") or pat.split('/')[1] == "%prefix%": continue # do nothing, default subcriptions will match this topic else: self.topics += expand_fulltopic(pat) for d in self.env.devices: # if device has a non-standard pattern, check if the pattern is found in the custom patterns if not d.is_default() and d.p['FullTopic'] not in custom_patterns: # if pattern is not found then add the device topics to subscription list. # if the pattern is found, it will be matched without implicit subscription self.topics += expand_fulltopic(d.p['FullTopic']) # passing a list of tuples as recommended by paho self.mqtt.subscribe([(topic, 0) for topic in self.topics]) @pyqtSlot(str, str) def mqtt_publish(self, t, p): self.mqtt.publish(t, p) def mqtt_publish_queue(self): for q in self.mqtt_queue: t, p = q self.mqtt.publish(t, p) self.mqtt_queue.pop(self.mqtt_queue.index(q)) def mqtt_disconnected(self): self.actToggleConnect.setIcon(QIcon(":/disconnect.png")) self.actToggleConnect.setText("Connect") self.statusBar().showMessage("Disconnected") def mqtt_connectError(self, rc): reason = { 1: "Incorrect protocol version", 2: "Invalid client identifier", 3: "Server unavailable", 4: "Bad username or password", 5: "Not authorized", } self.statusBar().showMessage("Connection error: {}".format(reason[rc])) self.actToggleConnect.setChecked(False) def mqtt_message(self, topic, msg): # try to find a device by matching known FullTopics against the MQTT topic of the message device = self.env.find_device(topic) if device: if topic.endswith("LWT"): if not msg: msg = "Offline" device.update_property("LWT", msg) if msg == 'Online': # known device came online, query initial state self.initial_query(device, True) else: # forward the message for processing device.parse_message(topic, msg) if device.debug: logging.debug("MQTT: %s %s", topic, msg) else: # unknown device, start autodiscovery process if topic.endswith("LWT"): self.env.lwts.append(topic) logging.info("DISCOVERY: LWT from an unknown device %s", topic) # STAGE 1 # load default and user-provided FullTopic patterns and for all the patterns, # try matching the LWT topic (it follows the device's FullTopic syntax for p in default_patterns + custom_patterns: match = re.fullmatch( p.replace("%topic%", "(?P<topic>.*?)").replace( "%prefix%", "(?P<prefix>.*?)") + ".*$", topic) if match: # assume that the matched topic is the one configured in device settings possible_topic = match.groupdict().get('topic') if possible_topic not in ('tele', 'stat'): # if the assumed topic is different from tele or stat, there is a chance that it's a valid topic # query the assumed device for its FullTopic. False positives won't reply. possible_topic_cmnd = p.replace( "%prefix%", "cmnd").replace( "%topic%", possible_topic) + "FullTopic" logging.debug( "DISCOVERY: Asking an unknown device for FullTopic at %s", possible_topic_cmnd) self.mqtt_queue.append([possible_topic_cmnd, ""]) elif topic.endswith("RESULT") or topic.endswith( "FULLTOPIC"): # reply from an unknown device # STAGE 2 full_topic = loads(msg).get('FullTopic') if full_topic: # the device replies with its FullTopic # here the Topic is extracted using the returned FullTopic, identifying the device parsed = parse_topic(full_topic, topic) if parsed: # got a match, we query the device's MAC address in case it's a known device that had its topic changed logging.debug( "DISCOVERY: topic %s is matched by fulltopic %s", topic, full_topic) d = self.env.find_device(topic=parsed['topic']) if d: d.update_property("FullTopic", full_topic) else: logging.info( "DISCOVERY: Discovered topic=%s with fulltopic=%s", parsed['topic'], full_topic) d = TasmotaDevice(parsed['topic'], full_topic) self.env.devices.append(d) self.device_model.addDevice(d) logging.debug( "DISCOVERY: Sending initial query to topic %s", parsed['topic']) self.initial_query(d, True) self.env.lwts.remove(d.tele_topic("LWT")) d.update_property("LWT", "Online") def export(self): fname, _ = QFileDialog.getSaveFileName(self, "Export device list as...", directory=QDir.homePath(), filter="CSV files (*.csv)") if fname: if not fname.endswith(".csv"): fname += ".csv" with open(fname, "w", encoding='utf8') as f: column_titles = [ 'mac', 'topic', 'friendly_name', 'full_topic', 'cmnd_topic', 'stat_topic', 'tele_topic', 'module', 'module_id', 'firmware', 'core' ] c = csv.writer(f) c.writerow(column_titles) for r in range(self.device_model.rowCount()): d = self.device_model.index(r, 0) c.writerow([ self.device_model.mac(d), self.device_model.topic(d), self.device_model.friendly_name(d), self.device_model.fullTopic(d), self.device_model.commandTopic(d), self.device_model.statTopic(d), self.device_model.teleTopic(d), # modules.get(self.device_model.module(d)), self.device_model.module(d), self.device_model.firmware(d), self.device_model.core(d) ]) def bssid(self): BSSIdDialog().exec_() def patterns(self): PatternsDialog().exec_() # def openhab(self): # OpenHABDialog(self.env).exec_() def showSubs(self): QMessageBox.information(self, "Subscriptions", "\n".join(sorted(self.topics))) def clear_LWT(self): dlg = ClearLWTDialog(self.env) if dlg.exec_() == ClearLWTDialog.Accepted: for row in range(dlg.lw.count()): itm = dlg.lw.item(row) if itm.checkState() == Qt.Checked: topic = itm.text() self.mqtt.publish(topic, retain=True) self.env.lwts.remove(topic) logging.info("MQTT: Cleared %s", topic) def prefs(self): dlg = PrefsDialog() if dlg.exec_() == QDialog.Accepted: update_devices = False devices_short_version = self.settings.value( "devices_short_version", True, bool) if devices_short_version != dlg.cbDevShortVersion.isChecked(): update_devices = True self.settings.setValue("devices_short_version", dlg.cbDevShortVersion.isChecked()) update_consoles = False console_font_size = self.settings.value("console_font_size", 9) if console_font_size != dlg.sbConsFontSize.value(): update_consoles = True self.settings.setValue("console_font_size", dlg.sbConsFontSize.value()) console_word_wrap = self.settings.value("console_word_wrap", True, bool) if console_word_wrap != dlg.cbConsWW.isChecked(): update_consoles = True self.settings.setValue("console_word_wrap", dlg.cbConsWW.isChecked()) if update_consoles: for c in self.consoles: c.console.setWordWrapMode(dlg.cbConsWW.isChecked()) new_font = QFont(c.console.font()) new_font.setPointSize(dlg.sbConsFontSize.value()) c.console.setFont(new_font) self.settings.sync() def auto_telemetry_period(self): curr_val = self.settings.value("autotelemetry", 5000, int) period, ok = QInputDialog.getInt( self, "Set AutoTelemetry period", "Values under 5000ms may cause increased ESP LoadAvg", curr_val, 1000) if ok: self.settings.setValue("autotelemetry", period) self.settings.sync() @pyqtSlot(TasmotaDevice) def selectDevice(self, d): self.device = d @pyqtSlot() def openTelemetry(self): if self.device: tele_widget = TelemetryWidget(self.device) self.addDockWidget(Qt.RightDockWidgetArea, tele_widget) self.mqtt_publish(self.device.cmnd_topic('STATUS'), "8") @pyqtSlot() def openConsole(self): if self.device: console_widget = ConsoleWidget(self.device) self.mqtt.messageSignal.connect(console_widget.consoleAppend) console_widget.sendCommand.connect(self.mqtt.publish) self.addDockWidget(Qt.BottomDockWidgetArea, console_widget) console_widget.command.setFocus() self.consoles.append(console_widget) @pyqtSlot() def openRulesEditor(self): if self.device: rules = RulesWidget(self.device) self.mqtt.messageSignal.connect(rules.parseMessage) rules.sendCommand.connect(self.mqtt_publish) self.mdi.setViewMode(QMdiArea.TabbedView) self.mdi.addSubWindow(rules) rules.setWindowState(Qt.WindowMaximized) rules.destroyed.connect(self.updateMDI) self.mqtt_queue.append((self.device.cmnd_topic("ruletimer"), "")) self.mqtt_queue.append((self.device.cmnd_topic("rule1"), "")) self.mqtt_queue.append((self.device.cmnd_topic("Var"), "")) self.mqtt_queue.append((self.device.cmnd_topic("Mem"), "")) @pyqtSlot() def openWebUI(self): if self.device and self.device.p.get('IPAddress'): url = QUrl("http://{}".format(self.device.p['IPAddress'])) try: webui = QWebEngineView() webui.load(url) frm_webui = QFrame() frm_webui.setWindowTitle("WebUI [{}]".format( self.device.p['FriendlyName1'])) frm_webui.setFrameShape(QFrame.StyledPanel) frm_webui.setLayout(VLayout(0)) frm_webui.layout().addWidget(webui) frm_webui.destroyed.connect(self.updateMDI) self.mdi.addSubWindow(frm_webui) self.mdi.setViewMode(QMdiArea.TabbedView) frm_webui.setWindowState(Qt.WindowMaximized) except NameError: QDesktopServices.openUrl( QUrl("http://{}".format(self.device.p['IPAddress']))) def updateMDI(self): if len(self.mdi.subWindowList()) == 1: self.mdi.setViewMode(QMdiArea.SubWindowView) self.devices_list.setWindowState(Qt.WindowMaximized) def closeEvent(self, e): self.settings.setValue("version", self._version) self.settings.setValue("window_geometry", self.saveGeometry()) self.settings.setValue("views_order", ";".join(self.devices_list.views.keys())) self.settings.beginGroup("Views") for view, items in self.devices_list.views.items(): self.settings.setValue(view, ";".join(items[1:])) self.settings.endGroup() self.settings.sync() for d in self.env.devices: mac = d.p.get('Mac') topic = d.p['Topic'] full_topic = d.p['FullTopic'] friendly_name = d.p['FriendlyName1'] if mac: self.devices.beginGroup(mac.replace(":", "-")) self.devices.setValue("topic", topic) self.devices.setValue("full_topic", full_topic) self.devices.setValue("friendly_name", friendly_name) for i, h in enumerate(d.history): self.devices.setValue("history/{}".format(i), h) self.devices.endGroup() self.devices.sync() e.accept()
class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self._version = "0.1.20" self.setWindowIcon(QIcon("GUI/icons/logo.png")) self.setWindowTitle("Tasmota Device Manager {}".format(self._version)) self.main_splitter = QSplitter() self.devices_splitter = QSplitter(Qt.Vertical) self.mqtt_queue = [] self.devices = {} self.fulltopic_queue = [] old_settings = QSettings() self.settings = QSettings("{}/TDM/tdm.cfg".format(QDir.homePath()), QSettings.IniFormat) self.setMinimumSize(QSize(1280, 800)) for k in old_settings.allKeys(): self.settings.setValue(k, old_settings.value(k)) old_settings.remove(k) self.device_model = TasmotaDevicesModel() self.telemetry_model = TasmotaDevicesTree() self.console_model = ConsoleModel() self.sorted_console_model = QSortFilterProxyModel() self.sorted_console_model.setSourceModel(self.console_model) self.sorted_console_model.setFilterKeyColumn(CnsMdl.FRIENDLY_NAME) self.setup_mqtt() self.setup_telemetry_view() self.setup_main_layout() self.add_devices_tab() self.build_toolbars() self.setStatusBar(QStatusBar()) self.queue_timer = QTimer() self.queue_timer.timeout.connect(self.mqtt_publish_queue) self.queue_timer.start(500) self.auto_timer = QTimer() self.auto_timer.timeout.connect(self.autoupdate) self.load_window_state() if self.settings.value("connect_on_startup", False, bool): self.actToggleConnect.trigger() def setup_main_layout(self): self.mdi = QMdiArea() self.mdi.setActivationOrder(QMdiArea.ActivationHistoryOrder) self.mdi.setViewMode(QMdiArea.TabbedView) self.mdi.setDocumentMode(True) mdi_widget = QWidget() mdi_widget.setLayout(VLayout()) mdi_widget.layout().addWidget(self.mdi) self.devices_splitter.addWidget(mdi_widget) vl_console = VLayout() hl_filter = HLayout() self.cbFilter = QCheckBox("Console filtering") self.cbxFilterDevice = QComboBox() self.cbxFilterDevice.setEnabled(False) self.cbxFilterDevice.setFixedWidth(200) self.cbxFilterDevice.setModel(self.device_model) self.cbxFilterDevice.setModelColumn(DevMdl.FRIENDLY_NAME) hl_filter.addWidgets([self.cbFilter, self.cbxFilterDevice]) hl_filter.addStretch(0) vl_console.addLayout(hl_filter) self.console_view = TableView() self.console_view.setModel(self.console_model) self.console_view.setupColumns(columns_console) self.console_view.setAlternatingRowColors(True) self.console_view.verticalHeader().setDefaultSectionSize(20) self.console_view.setMinimumHeight(200) vl_console.addWidget(self.console_view) console_widget = QWidget() console_widget.setLayout(vl_console) self.devices_splitter.addWidget(console_widget) self.main_splitter.insertWidget(0, self.devices_splitter) self.setCentralWidget(self.main_splitter) self.console_view.clicked.connect(self.select_cons_entry) self.console_view.doubleClicked.connect(self.view_payload) self.cbFilter.toggled.connect(self.toggle_console_filter) self.cbxFilterDevice.currentTextChanged.connect( self.select_console_filter) def setup_telemetry_view(self): tele_widget = QWidget() vl_tele = VLayout() self.tview = QTreeView() self.tview.setMinimumWidth(300) self.tview.setModel(self.telemetry_model) self.tview.setAlternatingRowColors(True) self.tview.setUniformRowHeights(True) self.tview.setIndentation(15) self.tview.setSizePolicy( QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)) self.tview.expandAll() self.tview.resizeColumnToContents(0) vl_tele.addWidget(self.tview) tele_widget.setLayout(vl_tele) self.main_splitter.addWidget(tele_widget) def setup_mqtt(self): self.mqtt = MqttClient() self.mqtt.connecting.connect(self.mqtt_connecting) self.mqtt.connected.connect(self.mqtt_connected) self.mqtt.disconnected.connect(self.mqtt_disconnected) self.mqtt.connectError.connect(self.mqtt_connectError) self.mqtt.messageSignal.connect(self.mqtt_message) def add_devices_tab(self): tabDevicesList = DevicesListWidget(self) self.mdi.addSubWindow(tabDevicesList) tabDevicesList.setWindowState(Qt.WindowMaximized) def load_window_state(self): wndGeometry = self.settings.value('window_geometry') if wndGeometry: self.restoreGeometry(wndGeometry) spltState = self.settings.value('splitter_state') if spltState: self.main_splitter.restoreState(spltState) def build_toolbars(self): main_toolbar = Toolbar(orientation=Qt.Horizontal, iconsize=16, label_position=Qt.ToolButtonTextBesideIcon) main_toolbar.setObjectName("main_toolbar") self.addToolBar(main_toolbar) main_toolbar.addAction(QIcon("./GUI/icons/connections.png"), "Broker", self.setup_broker) self.actToggleConnect = QAction(QIcon("./GUI/icons/disconnect.png"), "MQTT") self.actToggleConnect.setCheckable(True) self.actToggleConnect.toggled.connect(self.toggle_connect) main_toolbar.addAction(self.actToggleConnect) self.actToggleAutoUpdate = QAction(QIcon("./GUI/icons/automatic.png"), "Auto telemetry") self.actToggleAutoUpdate.setCheckable(True) self.actToggleAutoUpdate.toggled.connect(self.toggle_autoupdate) main_toolbar.addAction(self.actToggleAutoUpdate) main_toolbar.addSeparator() main_toolbar.addAction(QIcon("./GUI/icons/bssid.png"), "BSSId", self.bssid) main_toolbar.addAction(QIcon("./GUI/icons/export.png"), "Export list", self.export) def initial_query(self, idx, queued=False): for q in initial_queries: topic = "{}status".format(self.device_model.commandTopic(idx)) if queued: self.mqtt_queue.append([topic, q]) else: self.mqtt.publish(topic, q, 1) self.console_log(topic, "Asked for STATUS {}".format(q), q) def setup_broker(self): brokers_dlg = BrokerDialog() if brokers_dlg.exec_( ) == QDialog.Accepted and self.mqtt.state == self.mqtt.Connected: self.mqtt.disconnect() def toggle_autoupdate(self, state): if state: self.auto_timer.setInterval(5000) self.auto_timer.start() def toggle_connect(self, state): if state and self.mqtt.state == self.mqtt.Disconnected: self.broker_hostname = self.settings.value('hostname', 'localhost') self.broker_port = self.settings.value('port', 1883, int) self.broker_username = self.settings.value('username') self.broker_password = self.settings.value('password') self.mqtt.hostname = self.broker_hostname self.mqtt.port = self.broker_port if self.broker_username: self.mqtt.setAuth(self.broker_username, self.broker_password) self.mqtt.connectToHost() elif not state and self.mqtt.state == self.mqtt.Connected: self.mqtt_disconnect() def autoupdate(self): if self.mqtt.state == self.mqtt.Connected: for d in range(self.device_model.rowCount()): idx = self.device_model.index(d, 0) cmnd = self.device_model.commandTopic(idx) self.mqtt.publish(cmnd + "STATUS", payload=8) def mqtt_connect(self): self.broker_hostname = self.settings.value('hostname', 'localhost') self.broker_port = self.settings.value('port', 1883, int) self.broker_username = self.settings.value('username') self.broker_password = self.settings.value('password') self.mqtt.hostname = self.broker_hostname self.mqtt.port = self.broker_port if self.broker_username: self.mqtt.setAuth(self.broker_username, self.broker_password) if self.mqtt.state == self.mqtt.Disconnected: self.mqtt.connectToHost() def mqtt_disconnect(self): self.mqtt.disconnectFromHost() def mqtt_connecting(self): self.statusBar().showMessage("Connecting to broker") def mqtt_connected(self): self.actToggleConnect.setIcon(QIcon("./GUI/icons/connect.png")) self.statusBar().showMessage("Connected to {}:{} as {}".format( self.broker_hostname, self.broker_port, self.broker_username if self.broker_username else '[anonymous]')) self.mqtt_subscribe() for d in range(self.device_model.rowCount()): idx = self.device_model.index(d, 0) self.initial_query(idx) def mqtt_subscribe(self): main_topics = ["+/stat/+", "+/tele/+", "stat/#", "tele/#"] for d in range(self.device_model.rowCount()): idx = self.device_model.index(d, 0) if not self.device_model.isDefaultTemplate(idx): main_topics.append(self.device_model.commandTopic(idx)) main_topics.append(self.device_model.statTopic(idx)) for t in main_topics: self.mqtt.subscribe(t) def mqtt_publish_queue(self): for q in self.mqtt_queue: t, p = q self.mqtt.publish(t, p) self.mqtt_queue.pop(self.mqtt_queue.index(q)) def mqtt_disconnected(self): self.actToggleConnect.setIcon(QIcon("./GUI/icons/disconnect.png")) self.statusBar().showMessage("Disconnected") def mqtt_connectError(self, rc): reason = { 1: "Incorrect protocol version", 2: "Invalid client identifier", 3: "Server unavailable", 4: "Bad username or password", 5: "Not authorized", } self.statusBar().showMessage("Connection error: {}".format(reason[rc])) self.actToggleConnect.setChecked(False) def mqtt_message(self, topic, msg): found = self.device_model.findDevice(topic) if found.reply == 'LWT': if not msg: msg = "offline" if found.index.isValid(): self.console_log(topic, "LWT update: {}".format(msg), msg) self.device_model.updateValue(found.index, DevMdl.LWT, msg) self.initial_query(found.index, queued=True) elif msg == "Online": self.console_log( topic, "LWT for unknown device '{}'. Asking for FullTopic.". format(found.topic), msg, False) self.mqtt_queue.append( ["cmnd/{}/fulltopic".format(found.topic), ""]) self.mqtt_queue.append( ["{}/cmnd/fulltopic".format(found.topic), ""]) elif found.reply == 'RESULT': try: full_topic = loads(msg).get('FullTopic') new_topic = loads(msg).get('Topic') template_name = loads(msg).get('NAME') ota_url = loads(msg).get('OtaUrl') teleperiod = loads(msg).get('TelePeriod') if full_topic: # TODO: update FullTopic for existing device AFTER the FullTopic changes externally (the message will arrive from new FullTopic) if not found.index.isValid(): self.console_log( topic, "FullTopic for {}".format(found.topic), msg, False) new_idx = self.device_model.addDevice(found.topic, full_topic, lwt='online') tele_idx = self.telemetry_model.addDevice( TasmotaDevice, found.topic) self.telemetry_model.devices[found.topic] = tele_idx #TODO: add QSortFilterProxyModel to telemetry treeview and sort devices after adding self.initial_query(new_idx) self.console_log( topic, "Added {} with fulltopic {}, querying for STATE". format(found.topic, full_topic), msg) self.tview.expand(tele_idx) self.tview.resizeColumnToContents(0) elif new_topic: if found.index.isValid() and found.topic != new_topic: self.console_log( topic, "New topic for {}".format(found.topic), msg) self.device_model.updateValue(found.index, DevMdl.TOPIC, new_topic) tele_idx = self.telemetry_model.devices.get( found.topic) if tele_idx: self.telemetry_model.setDeviceName( tele_idx, new_topic) self.telemetry_model.devices[ new_topic] = self.telemetry_model.devices.pop( found.topic) elif template_name: self.device_model.updateValue( found.index, DevMdl.MODULE, "{} (0)".format(template_name)) elif ota_url: self.device_model.updateValue(found.index, DevMdl.OTA_URL, ota_url) elif teleperiod: self.device_model.updateValue(found.index, DevMdl.TELEPERIOD, teleperiod) except JSONDecodeError as e: self.console_log( topic, "JSON payload decode error. Check error.log for additional info." ) with open("{}/TDM/error.log".format(QDir.homePath()), "a+") as l: l.write("{}\t{}\t{}\t{}\n".format( QDateTime.currentDateTime().toString( "yyyy-MM-dd hh:mm:ss"), topic, msg, e.msg)) elif found.index.isValid(): ok = False try: if msg.startswith("{"): payload = loads(msg) else: payload = msg ok = True except JSONDecodeError as e: self.console_log( topic, "JSON payload decode error. Check error.log for additional info." ) with open("{}/TDM/error.log".format(QDir.homePath()), "a+") as l: l.write("{}\t{}\t{}\t{}\n".format( QDateTime.currentDateTime().toString( "yyyy-MM-dd hh:mm:ss"), topic, msg, e.msg)) if ok: try: if found.reply == 'STATUS': self.console_log(topic, "Received device status", msg) payload = payload['Status'] self.device_model.updateValue( found.index, DevMdl.FRIENDLY_NAME, payload['FriendlyName'][0]) self.telemetry_model.setDeviceFriendlyName( self.telemetry_model.devices[found.topic], payload['FriendlyName'][0]) module = payload['Module'] if module == 0: self.mqtt.publish( self.device_model.commandTopic(found.index) + "template") else: self.device_model.updateValue( found.index, DevMdl.MODULE, modules.get(module, 'Unknown')) self.device_model.updateValue(found.index, DevMdl.MODULE_ID, module) elif found.reply == 'STATUS1': self.console_log(topic, "Received program information", msg) payload = payload['StatusPRM'] self.device_model.updateValue( found.index, DevMdl.RESTART_REASON, payload.get('RestartReason')) self.device_model.updateValue(found.index, DevMdl.OTA_URL, payload.get('OtaUrl')) elif found.reply == 'STATUS2': self.console_log(topic, "Received firmware information", msg) payload = payload['StatusFWR'] self.device_model.updateValue(found.index, DevMdl.FIRMWARE, payload['Version']) self.device_model.updateValue(found.index, DevMdl.CORE, payload['Core']) elif found.reply == 'STATUS3': self.console_log(topic, "Received syslog information", msg) payload = payload['StatusLOG'] self.device_model.updateValue(found.index, DevMdl.TELEPERIOD, payload['TelePeriod']) elif found.reply == 'STATUS5': self.console_log(topic, "Received network status", msg) payload = payload['StatusNET'] self.device_model.updateValue(found.index, DevMdl.MAC, payload['Mac']) self.device_model.updateValue(found.index, DevMdl.IP, payload['IPAddress']) elif found.reply in ('STATE', 'STATUS11'): self.console_log(topic, "Received device state", msg) if found.reply == 'STATUS11': payload = payload['StatusSTS'] self.parse_state(found.index, payload) elif found.reply in ('SENSOR', 'STATUS8'): self.console_log(topic, "Received telemetry", msg) if found.reply == 'STATUS8': payload = payload['StatusSNS'] self.parse_telemetry(found.index, payload) elif found.reply.startswith('POWER'): self.console_log( topic, "Received {} state".format(found.reply), msg) payload = {found.reply: msg} self.parse_power(found.index, payload) except KeyError as k: self.console_log( topic, "JSON key error. Check error.log for additional info.") with open("{}/TDM/error.log".format(QDir.homePath()), "a+") as l: l.write("{}\t{}\t{}\tKeyError: {}\n".format( QDateTime.currentDateTime().toString( "yyyy-MM-dd hh:mm:ss"), topic, payload, k.args[0])) def parse_power(self, index, payload, from_state=False): old = self.device_model.power(index) power = { k: payload[k] for k in payload.keys() if k.startswith("POWER") } # TODO: fix so that number of relays get updated properly after module/no. of relays change needs_update = False if old: # if from_state and len(old) != len(power): # needs_update = True # # else: for k in old.keys(): needs_update |= old[k] != power.get(k, old[k]) if needs_update: break else: needs_update = True if needs_update: self.device_model.updateValue(index, DevMdl.POWER, power) def parse_state(self, index, payload): bssid = payload['Wifi'].get('BSSId') if not bssid: bssid = payload['Wifi'].get('APMac') self.device_model.updateValue(index, DevMdl.BSSID, bssid) self.device_model.updateValue(index, DevMdl.SSID, payload['Wifi']['SSId']) self.device_model.updateValue(index, DevMdl.CHANNEL, payload['Wifi'].get('Channel', "n/a")) self.device_model.updateValue(index, DevMdl.RSSI, payload['Wifi']['RSSI']) self.device_model.updateValue(index, DevMdl.UPTIME, payload['Uptime']) self.device_model.updateValue(index, DevMdl.LOADAVG, payload.get('LoadAvg')) self.device_model.updateValue(index, DevMdl.LINKCOUNT, payload['Wifi'].get('LinkCount', "n/a")) self.device_model.updateValue(index, DevMdl.DOWNTIME, payload['Wifi'].get('Downtime', "n/a")) self.parse_power(index, payload, True) tele_idx = self.telemetry_model.devices.get( self.device_model.topic(index)) if tele_idx: tele_device = self.telemetry_model.getNode(tele_idx) self.telemetry_model.setDeviceFriendlyName( tele_idx, self.device_model.friendly_name(index)) pr = tele_device.provides() for k in pr.keys(): self.telemetry_model.setData(pr[k], payload.get(k)) def parse_telemetry(self, index, payload): device = self.telemetry_model.devices.get( self.device_model.topic(index)) if device: node = self.telemetry_model.getNode(device) time = node.provides()['Time'] if 'Time' in payload: self.telemetry_model.setData(time, payload.pop('Time')) temp_unit = "C" pres_unit = "hPa" if 'TempUnit' in payload: temp_unit = payload.pop('TempUnit') if 'PressureUnit' in payload: pres_unit = payload.pop('PressureUnit') for sensor in sorted(payload.keys()): if sensor == 'DS18x20': for sns_name in payload[sensor].keys(): d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice( DS18x20, payload[sensor][sns_name]['Type'], device) self.telemetry_model.getNode(d).setTempUnit(temp_unit) payload[sensor][sns_name]['Id'] = payload[sensor][ sns_name].pop('Address') pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData( pr[pk], payload[sensor][sns_name].get(pk)) self.tview.expand(d) elif sensor.startswith('DS18B20'): d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice( DS18x20, sensor, device) self.telemetry_model.getNode(d).setTempUnit(temp_unit) pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) if sensor == 'COUNTER': d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice( CounterSns, "Counter", device) pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) else: d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice( sensor_map.get(sensor, Node), sensor, device) pr = self.telemetry_model.getNode(d).provides() if 'Temperature' in pr: self.telemetry_model.getNode(d).setTempUnit(temp_unit) if 'Pressure' in pr or 'SeaPressure' in pr: self.telemetry_model.getNode(d).setPresUnit(pres_unit) for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) # self.tview.resizeColumnToContents(0) def console_log(self, topic, description, payload="", known=True): longest_tp = 0 longest_fn = 0 short_topic = "/".join(topic.split("/")[0:-1]) fname = self.devices.get(short_topic, "") if not fname: device = self.device_model.findDevice(topic) fname = self.device_model.friendly_name(device.index) self.devices.update({short_topic: fname}) self.console_model.addEntry(topic, fname, description, payload, known) if len(topic) > longest_tp: longest_tp = len(topic) self.console_view.resizeColumnToContents(1) if len(fname) > longest_fn: longest_fn = len(fname) self.console_view.resizeColumnToContents(1) def view_payload(self, idx): if self.cbFilter.isChecked(): idx = self.sorted_console_model.mapToSource(idx) row = idx.row() timestamp = self.console_model.data( self.console_model.index(row, CnsMdl.TIMESTAMP)) topic = self.console_model.data( self.console_model.index(row, CnsMdl.TOPIC)) payload = self.console_model.data( self.console_model.index(row, CnsMdl.PAYLOAD)) dlg = PayloadViewDialog(timestamp, topic, payload) dlg.exec_() def select_cons_entry(self, idx): self.cons_idx = idx def export(self): fname, _ = QFileDialog.getSaveFileName(self, "Export device list as...", directory=QDir.homePath(), filter="CSV files (*.csv)") if fname: if not fname.endswith(".csv"): fname += ".csv" with open(fname, "w", encoding='utf8') as f: column_titles = [ 'mac', 'topic', 'friendly_name', 'full_topic', 'cmnd_topic', 'stat_topic', 'tele_topic', 'module', 'module_id', 'firmware', 'core' ] c = csv.writer(f) c.writerow(column_titles) for r in range(self.device_model.rowCount()): d = self.device_model.index(r, 0) c.writerow([ self.device_model.mac(d), self.device_model.topic(d), self.device_model.friendly_name(d), self.device_model.fullTopic(d), self.device_model.commandTopic(d), self.device_model.statTopic(d), self.device_model.teleTopic(d), modules.get(self.device_model.module(d)), self.device_model.module(d), self.device_model.firmware(d), self.device_model.core(d) ]) def bssid(self): BSSIdDialog().exec_() # if dlg.exec_() == QDialog.Accepted: def toggle_console_filter(self, state): self.cbxFilterDevice.setEnabled(state) if state: self.console_view.setModel(self.sorted_console_model) else: self.console_view.setModel(self.console_model) def select_console_filter(self, fname): self.sorted_console_model.setFilterFixedString(fname) def closeEvent(self, e): self.settings.setValue("window_geometry", self.saveGeometry()) self.settings.setValue("splitter_state", self.main_splitter.saveState()) self.settings.sync() e.accept()
class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) self._version = "0.1.11" self.setWindowIcon(QIcon("GUI/icons/logo.png")) self.setWindowTitle("Tasmota Device Manager {}".format(self._version)) self.main_splitter = QSplitter() self.devices_splitter = QSplitter(Qt.Vertical) self.fulltopic_queue = [] self.settings = QSettings() self.setMinimumSize(QSize(1280,800)) self.device_model = TasmotaDevicesModel() self.telemetry_model = TasmotaDevicesTree() self.console_model = ConsoleModel() self.sorted_console_model = QSortFilterProxyModel() self.sorted_console_model.setSourceModel(self.console_model) self.sorted_console_model.setFilterKeyColumn(CnsMdl.FRIENDLY_NAME) self.setup_mqtt() self.setup_telemetry_view() self.setup_main_layout() self.add_devices_tab() self.build_toolbars() self.setStatusBar(QStatusBar()) self.queue_timer = QTimer() self.queue_timer.setSingleShot(True) self.queue_timer.timeout.connect(self.mqtt_ask_for_fulltopic) self.build_cons_ctx_menu() self.load_window_state() def setup_main_layout(self): self.mdi = QMdiArea() self.mdi.setActivationOrder(QMdiArea.ActivationHistoryOrder) self.mdi.setViewMode(QMdiArea.TabbedView) self.mdi.setDocumentMode(True) mdi_widget = QWidget() mdi_widget.setLayout(VLayout()) mdi_widget.layout().addWidget(self.mdi) self.devices_splitter.addWidget(mdi_widget) vl_console = VLayout() self.console_view = TableView() self.console_view.setModel(self.sorted_console_model) self.console_view.setupColumns(columns_console) self.console_view.setAlternatingRowColors(True) self.console_view.setSortingEnabled(True) self.console_view.sortByColumn(CnsMdl.TIMESTAMP, Qt.DescendingOrder) self.console_view.verticalHeader().setDefaultSectionSize(20) self.console_view.setMinimumHeight(200) self.console_view.setContextMenuPolicy(Qt.CustomContextMenu) vl_console.addWidget(self.console_view) console_widget = QWidget() console_widget.setLayout(vl_console) self.devices_splitter.addWidget(console_widget) self.main_splitter.insertWidget(0, self.devices_splitter) self.setCentralWidget(self.main_splitter) self.console_view.clicked.connect(self.select_cons_entry) self.console_view.doubleClicked.connect(self.view_payload) self.console_view.customContextMenuRequested.connect(self.show_cons_ctx_menu) def setup_telemetry_view(self): tele_widget = QWidget() vl_tele = VLayout() self.tview = QTreeView() self.tview.setMinimumWidth(300) self.tview.setModel(self.telemetry_model) self.tview.setAlternatingRowColors(True) self.tview.setUniformRowHeights(True) self.tview.setIndentation(15) self.tview.setSizePolicy(QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Minimum)) self.tview.expandAll() self.tview.resizeColumnToContents(0) vl_tele.addWidget(self.tview) tele_widget.setLayout(vl_tele) self.main_splitter.addWidget(tele_widget) def setup_mqtt(self): self.mqtt = MqttClient() self.mqtt.connecting.connect(self.mqtt_connecting) self.mqtt.connected.connect(self.mqtt_connected) self.mqtt.disconnected.connect(self.mqtt_disconnected) self.mqtt.connectError.connect(self.mqtt_connectError) self.mqtt.messageSignal.connect(self.mqtt_message) def add_devices_tab(self): tabDevicesList = DevicesListWidget(self) self.mdi.addSubWindow(tabDevicesList) tabDevicesList.setWindowState(Qt.WindowMaximized) def load_window_state(self): wndGeometry = self.settings.value('window_geometry') if wndGeometry: self.restoreGeometry(wndGeometry) spltState = self.settings.value('splitter_state') if spltState: self.main_splitter.restoreState(spltState) def build_toolbars(self): main_toolbar = Toolbar(orientation=Qt.Horizontal, iconsize=32, label_position=Qt.ToolButtonIconOnly) main_toolbar.setObjectName("main_toolbar") self.addToolBar(main_toolbar) main_toolbar.addAction(QIcon("./GUI/icons/connections.png"), "Configure MQTT broker", self.setup_broker) agBroker = QActionGroup(self) agBroker.setExclusive(True) self.actConnect = CheckableAction(QIcon("./GUI/icons/connect.png"), "Connect to the broker", agBroker) self.actDisconnect = CheckableAction(QIcon("./GUI/icons/disconnect.png"), "Disconnect from broker", agBroker) self.actDisconnect.setChecked(True) self.actConnect.triggered.connect(self.mqtt_connect) self.actDisconnect.triggered.connect(self.mqtt_disconnect) main_toolbar.addActions(agBroker.actions()) main_toolbar.addSeparator() def initial_query(self, idx): for q in initial_queries: topic = "{}status".format(self.device_model.commandTopic(idx)) self.mqtt.publish(topic, q) q = q if q else '' self.console_log(topic, "Asked for STATUS {}".format(q), q) def setup_broker(self): brokers_dlg = BrokerDialog() if brokers_dlg.exec_() == QDialog.Accepted and self.mqtt.state == self.mqtt.Connected: self.mqtt.disconnect() def mqtt_connect(self): self.broker_hostname = self.settings.value('hostname', 'localhost') self.broker_port = self.settings.value('port', 1883, int) self.broker_username = self.settings.value('username') self.broker_password = self.settings.value('password') self.mqtt.hostname = self.broker_hostname self.mqtt.port = self.broker_port if self.broker_username: self.mqtt.setAuth(self.broker_username, self.broker_password) if self.mqtt.state == self.mqtt.Disconnected: self.mqtt.connectToHost() def mqtt_disconnect(self): self.mqtt.disconnectFromHost() def mqtt_connecting(self): self.statusBar().showMessage("Connecting to broker") def mqtt_connected(self): self.statusBar().showMessage("Connected to {}:{} as {}".format(self.broker_hostname, self.broker_port, self.broker_username if self.broker_username else '[anonymous]')) self.mqtt_subscribe() for d in range(self.device_model.rowCount()): idx = self.device_model.index(d, 0) self.initial_query(idx) def mqtt_subscribe(self): main_topics = ["+/stat/+", "+/tele/+", "stat/#", "tele/#"] for d in range(self.device_model.rowCount()): idx = self.device_model.index(d, 0) if not self.device_model.isDefaultTemplate(idx): main_topics.append(self.device_model.commandTopic(idx)) main_topics.append(self.device_model.statTopic(idx)) for t in main_topics: self.mqtt.subscribe(t) def mqtt_ask_for_fulltopic(self): for i in range(len(self.fulltopic_queue)): self.mqtt.publish(self.fulltopic_queue.pop(0)) def mqtt_disconnected(self): self.statusBar().showMessage("Disconnected") def mqtt_connectError(self, rc): reason = { 1: "Incorrect protocol version", 2: "Invalid client identifier", 3: "Server unavailable", 4: "Bad username or password", 5: "Not authorized", } self.statusBar().showMessage("Connection error: {}".format(reason[rc])) self.actDisconnect.setChecked(True) def mqtt_message(self, topic, msg): found = self.device_model.findDevice(topic) if found.reply == 'LWT': if not msg: msg = "offline" if found.index.isValid(): self.console_log(topic, "LWT update: {}".format(msg), msg) self.device_model.updateValue(found.index, DevMdl.LWT, msg) elif msg == "Online": self.console_log(topic, "LWT for unknown device '{}'. Asking for FullTopic.".format(found.topic), msg, False) self.fulltopic_queue.append("cmnd/{}/fulltopic".format(found.topic)) self.fulltopic_queue.append("{}/cmnd/fulltopic".format(found.topic)) self.queue_timer.start(1500) elif found.reply == 'RESULT': full_topic = loads(msg).get('FullTopic') new_topic = loads(msg).get('Topic') template_name = loads(msg).get('NAME') if full_topic: # TODO: update FullTopic for existing device AFTER the FullTopic changes externally (the message will arrive from new FullTopic) if not found.index.isValid(): self.console_log(topic, "FullTopic for {}".format(found.topic), msg, False) new_idx = self.device_model.addDevice(found.topic, full_topic, lwt='online') tele_idx = self.telemetry_model.addDevice(TasmotaDevice, found.topic) self.telemetry_model.devices[found.topic] = tele_idx #TODO: add QSortFilterProxyModel to telemetry treeview and sort devices after adding self.initial_query(new_idx) self.console_log(topic, "Added {} with fulltopic {}, querying for STATE".format(found.topic, full_topic), msg) self.tview.expand(tele_idx) self.tview.resizeColumnToContents(0) if new_topic: if found.index.isValid() and found.topic != new_topic: self.console_log(topic, "New topic for {}".format(found.topic), msg) self.device_model.updateValue(found.index, DevMdl.TOPIC, new_topic) tele_idx = self.telemetry_model.devices.get(found.topic) if tele_idx: self.telemetry_model.setDeviceName(tele_idx, new_topic) self.telemetry_model.devices[new_topic] = self.telemetry_model.devices.pop(found.topic) if template_name: self.device_model.updateValue(found.index, DevMdl.MODULE, template_name) elif found.index.isValid(): if found.reply == 'STATUS': self.console_log(topic, "Received device status", msg) payload = loads(msg)['Status'] self.device_model.updateValue(found.index, DevMdl.FRIENDLY_NAME, payload['FriendlyName'][0]) self.telemetry_model.setDeviceFriendlyName(self.telemetry_model.devices[found.topic], payload['FriendlyName'][0]) self.tview.resizeColumnToContents(0) module = payload['Module'] if module == '0': self.mqtt.publish(self.device_model.commandTopic(found.index)+"template") else: self.device_model.updateValue(found.index, DevMdl.MODULE, module) elif found.reply == 'STATUS1': self.console_log(topic, "Received program information", msg) payload = loads(msg)['StatusPRM'] self.device_model.updateValue(found.index, DevMdl.RESTART_REASON, payload['RestartReason']) elif found.reply == 'STATUS2': self.console_log(topic, "Received firmware information", msg) payload = loads(msg)['StatusFWR'] self.device_model.updateValue(found.index, DevMdl.FIRMWARE, payload['Version']) self.device_model.updateValue(found.index, DevMdl.CORE, payload['Core']) elif found.reply == 'STATUS3': self.console_log(topic, "Received syslog information", msg) payload = loads(msg)['StatusLOG'] self.device_model.updateValue(found.index, DevMdl.TELEPERIOD, payload['TelePeriod']) elif found.reply == 'STATUS5': self.console_log(topic, "Received network status", msg) payload = loads(msg)['StatusNET'] self.device_model.updateValue(found.index, DevMdl.MAC, payload['Mac']) self.device_model.updateValue(found.index, DevMdl.IP, payload['IPAddress']) elif found.reply == 'STATUS8': self.console_log(topic, "Received telemetry", msg) payload = loads(msg)['StatusSNS'] self.parse_telemetry(found.index, payload) elif found.reply == 'STATUS11': self.console_log(topic, "Received device state", msg) payload = loads(msg)['StatusSTS'] self.parse_state(found.index, payload) elif found.reply == 'SENSOR': self.console_log(topic, "Received telemetry", msg) payload = loads(msg) self.parse_telemetry(found.index, payload) elif found.reply == 'STATE': self.console_log(topic, "Received device state", msg) payload = loads(msg) self.parse_state(found.index, payload) elif found.reply.startswith('POWER'): self.console_log(topic, "Received {} state".format(found.reply), msg) payload = {found.reply: msg} self.parse_power(found.index, payload) def parse_power(self, index, payload): old = self.device_model.power(index) power = {k: payload[k] for k in payload.keys() if k.startswith("POWER")} needs_update = False if old: for k in old.keys(): needs_update |= old[k] != power.get(k, old[k]) if needs_update: break else: needs_update = True if needs_update: self.device_model.updateValue(index, DevMdl.POWER, power) def parse_state(self, index, payload): bssid = payload['Wifi'].get('BSSId') if not bssid: bssid = payload['Wifi'].get('APMac') self.device_model.updateValue(index, DevMdl.BSSID, bssid) self.device_model.updateValue(index, DevMdl.SSID, payload['Wifi']['SSId']) self.device_model.updateValue(index, DevMdl.CHANNEL, payload['Wifi'].get('Channel')) self.device_model.updateValue(index, DevMdl.RSSI, payload['Wifi']['RSSI']) self.device_model.updateValue(index, DevMdl.UPTIME, payload['Uptime']) self.device_model.updateValue(index, DevMdl.LOADAVG, payload.get('LoadAvg')) self.parse_power(index, payload) tele_idx = self.telemetry_model.devices.get(self.device_model.topic(index)) if tele_idx: tele_device = self.telemetry_model.getNode(tele_idx) self.telemetry_model.setDeviceFriendlyName(tele_idx, self.device_model.friendly_name(index)) pr = tele_device.provides() for k in pr.keys(): self.telemetry_model.setData(pr[k], payload.get(k)) def parse_telemetry(self, index, payload): device = self.telemetry_model.devices.get(self.device_model.topic(index)) if device: node = self.telemetry_model.getNode(device) time = node.provides()['Time'] if 'Time' in payload: self.telemetry_model.setData(time, payload.pop('Time')) temp_unit = "C" pres_unit = "hPa" if 'TempUnit' in payload: temp_unit = payload.pop('TempUnit') if 'PressureUnit' in payload: pres_unit = payload.pop('PressureUnit') for sensor in sorted(payload.keys()): if sensor == 'DS18x20': for sns_name in payload[sensor].keys(): d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice(DS18x20, payload[sensor][sns_name]['Type'], device) self.telemetry_model.getNode(d).setTempUnit(temp_unit) payload[sensor][sns_name]['Id'] = payload[sensor][sns_name].pop('Address') pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor][sns_name].get(pk)) self.tview.expand(d) elif sensor.startswith('DS18B20'): d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice(DS18x20, sensor, device) self.telemetry_model.getNode(d).setTempUnit(temp_unit) pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) if sensor == 'COUNTER': d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice(CounterSns, "Counter", device) pr = self.telemetry_model.getNode(d).provides() for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) else: d = node.devices().get(sensor) if not d: d = self.telemetry_model.addDevice(sensor_map.get(sensor, Node), sensor, device) pr = self.telemetry_model.getNode(d).provides() if 'Temperature' in pr: self.telemetry_model.getNode(d).setTempUnit(temp_unit) if 'Pressure' in pr or 'SeaPressure' in pr: self.telemetry_model.getNode(d).setPresUnit(pres_unit) for pk in pr.keys(): self.telemetry_model.setData(pr[pk], payload[sensor].get(pk)) self.tview.expand(d) self.tview.resizeColumnToContents(0) def console_log(self, topic, description, payload, known=True): device = self.device_model.findDevice(topic) fname = self.device_model.friendly_name(device.index) self.console_model.addEntry(topic, fname, description, payload, known) self.console_view.resizeColumnToContents(1) def view_payload(self, idx): idx = self.sorted_console_model.mapToSource(idx) row = idx.row() timestamp = self.console_model.data(self.console_model.index(row, CnsMdl.TIMESTAMP)) topic = self.console_model.data(self.console_model.index(row, CnsMdl.TOPIC)) payload = self.console_model.data(self.console_model.index(row, CnsMdl.PAYLOAD)) dlg = PayloadViewDialog(timestamp, topic, payload) dlg.exec_() def select_cons_entry(self, idx): self.cons_idx = idx def build_cons_ctx_menu(self): self.cons_ctx_menu = QMenu() self.cons_ctx_menu.addAction("View payload", lambda: self.view_payload(self.cons_idx)) self.cons_ctx_menu.addSeparator() self.cons_ctx_menu.addAction("Show only this device", lambda: self.cons_set_filter(self.cons_idx)) self.cons_ctx_menu.addAction("Show all devices", self.cons_set_filter) def show_cons_ctx_menu(self, at): self.select_cons_entry(self.console_view.indexAt(at)) self.cons_ctx_menu.popup(self.console_view.viewport().mapToGlobal(at)) def cons_set_filter(self, idx=None): if idx: idx = self.sorted_console_model.mapToSource(idx) topic = self.console_model.data(self.console_model.index(idx.row(), CnsMdl.FRIENDLY_NAME)) self.sorted_console_model.setFilterFixedString(topic) else: self.sorted_console_model.setFilterFixedString("") def closeEvent(self, e): self.settings.setValue("window_geometry", self.saveGeometry()) self.settings.setValue("splitter_state", self.main_splitter.saveState()) self.settings.sync() e.accept()
class DevicesConfigWidget(QWidget): def __init__(self, parent, topic, *args, **kwargs): super(DevicesConfigWidget, self).__init__(*args, **kwargs) self.settings = QSettings("{}/TDM/tdm.cfg".format(QDir.homePath()), QSettings.IniFormat) self.topic = topic self.full_topic = self.settings.value( 'Devices/{}/full_topic'.format(topic)) self.friendly_name = self.settings.value( 'Devices/{}/friendly_name'.format(topic)) self.cmnd_topic = self.full_topic.replace("%topic%", self.topic).replace( "%prefix%", 'cmnd') self.tele_topic = self.full_topic.replace( "%topic%", self.topic).replace("%prefix%", 'tele') + "+" self.stat_topic = self.full_topic.replace( "%topic%", self.topic).replace("%prefix%", 'stat') + "+" self.setWindowTitle(self.friendly_name) self.mqtt = MqttClient() self.module = None self.modules = [] self.gpios = [] self.supported_gpios = [] self.gpio_cb = [] self.timers = False self.setLayout(VLayout(margin=[0, 6, 0, 0], spacing=3)) self.lbModule = QLabel("Connecting...") fnt = self.lbModule.font() fnt.setPointSize(14) fnt.setBold(True) self.lbModule.setFont(fnt) self.lbModule.setAlignment(Qt.AlignCenter) self.lbModule.setMaximumHeight(25) self.layout().addWidget(self.lbModule) self.build_tabs() self.create_timers() def create_timers(self): self.auto_timer = QTimer() self.auto_timer.setInterval(1000) self.auto_timer.timeout.connect(self.auto) self.auto_timer.start() self.mqtt_timer = QTimer() self.mqtt_timer.setSingleShot(True) self.mqtt_timer.timeout.connect(self.setupMqtt) self.mqtt_timer.start(1000) def build_tabs(self): self.tabs = QTabWidget() tabInformation = self.tabInformation() self.tabs.addTab(tabInformation, "Information") tabModule = self.tabModule() self.tabs.addTab(tabModule, "Module and Firmware") self.rule_grps = [] tabRules = self.tabRules() self.tabs.addTab(tabRules, "Rules and Timers") self.pbRTSet.clicked.connect(self.saveRuleTimers) self.pbVMSet.clicked.connect(self.saveVarMem) self.rg.pbSave.clicked.connect(self.saveRule) self.tabs.currentChanged.connect(self.tabChanged) self.tabs.setEnabled(False) self.layout().addWidget(self.tabs) def auto(self): if self.mqtt.state == self.mqtt.Connected: if self.pbRTPoll.isChecked(): self.loadRuleTimers() if self.pbVMPoll.isChecked(): self.loadVarMem() def tabChanged(self, tab): if tab == 2: self.loadRule(self.rg.cbRule.currentIndex()) self.loadTimer(self.cbTimer.currentIndex()) self.loadRuleTimers() self.loadVarMem() def setupMqtt(self): self.mqtt.hostname = self.settings.value('hostname', 'localhost') self.mqtt.port = self.settings.value('port', 1883, int) if self.settings.value('username'): self.mqtt.setAuth(self.settings.value('username'), self.settings.value('password')) self.mqtt.connected.connect(self.mqtt_subscribe) self.mqtt.messageSignal.connect(self.mqtt_message) self.mqtt.connectToHost() def mqtt_subscribe(self): self.mqtt.subscribe(self.tele_topic) self.mqtt.subscribe(self.stat_topic) self.initial_query() self.tabs.setEnabled(True) def mqtt_message(self, topic, msg): match = match_topic(self.full_topic, topic) if match: match = match.groupdict() reply = match['reply'] try: msg = loads(msg) if reply == "RESULT": first = list(msg)[0] if first.startswith('Rule'): self.parseRule(msg) elif first == "T1": self.parseRuleTimer(msg) elif first.startswith('Var'): self.parseVarMem(msg, 0) elif first.startswith('Mem'): self.parseVarMem(msg, 1) elif first == "Module": self.module = msg[first] self.updateCBModules() elif first.startswith('Modules'): self.parseModules(msg) elif first.startswith( 'GPIO') and not first.startswith('GPIOs'): self.parse_available_gpio(msg) elif first.startswith('GPIOs'): self.parse_supported_peripherals(msg) elif not first.startswith("Timers") and first.startswith( "Timer"): self.parseTimer(msg[first]) elif first == "Timers": self.gbTimers.setChecked(msg[first] == "ON") elif first.startswith("Timers"): pass else: print(msg) elif reply in ("STATE", "STATUS11"): if reply == "STATUS11": msg = msg['StatusSTS'] self.wifi_model.item(0, 1).setText("{} ({})".format( msg['Wifi'].get('SSId', "n/a"), msg['Wifi'].get('RSSI', "n/a"))) self.power = { k: msg[k] for k in msg.keys() if k.startswith("POWER") } self.cbxTimerOut.clear() self.cbxTimerOut.addItems(self.power) elif reply == "STATUS": msg = msg['Status'] self.lbModule.setText(modules.get(msg.get("Module"))) fname = msg['FriendlyName'] if isinstance(fname, str): fname = [fname] for i, fn in enumerate(fname): self.program_model.item(6 + i, 1).setText( msg['FriendlyName'][i]) self.mqtt_model.item(4, 1).setText("{}".format( msg.get('Topic', "n/a"))) elif reply == "STATUS1": msg = msg['StatusPRM'] self.program_model.item(3, 1).setText("{} @{}".format( msg.get('SaveCount', "n/a"), msg.get('SaveAddress', "n/a"))) self.program_model.item(4, 1).setText("{}".format( msg.get('BootCount', "n/a"))) self.program_model.item(5, 1).setText("{}".format( msg.get('RestartReason', "n/a"))) self.mqtt_model.item(5, 1).setText("{}".format( msg.get('GroupTopic', "n/a"))) elif reply == "STATUS2": msg = msg['StatusFWR'] self.program_model.item(0, 1).setText("{}".format( msg.get('Version', "n/a"))) self.program_model.item(1, 1).setText("{}".format( msg.get('BuildDateTime', "n/a"))) self.program_model.item(2, 1).setText("{} / {}".format( msg.get('Core', "n/a"), msg.get('SDK', "n/a"))) elif reply == "STATUS3": msg = msg['StatusLOG'] elif reply == "STATUS4": msg = msg['StatusMEM'] self.esp_model.item(0, 1).setText("n/a") self.esp_model.item(1, 1).setText("{}".format( msg.get('FlashChipId', "n/a"))) self.esp_model.item(2, 1).setText("{}".format( msg.get('FlashSize', "n/a"))) self.esp_model.item(3, 1).setText("{}".format( msg.get('ProgramFlashSize', "n/a"))) self.esp_model.item(4, 1).setText("n/a") self.esp_model.item(5, 1).setText("{}".format( msg.get('Free', "n/a"))) self.esp_model.item(6, 1).setText("{}".format( msg.get('Heap', "n/a"))) elif reply == "STATUS5": msg = msg['StatusNET'] self.wifi_model.item(1, 1).setText("{}".format( msg.get('Hostname', "n/a"))) self.wifi_model.item(2, 1).setText("{}".format( msg.get('IPAddress', "n/a"))) self.wifi_model.item(3, 1).setText("{}".format( msg.get('Gateway', "n/a"))) self.wifi_model.item(4, 1).setText("{}".format( msg.get('Subnetmask', "n/a"))) self.wifi_model.item(5, 1).setText("{}".format( msg.get('DNSServer', "n/a"))) self.wifi_model.item(6, 1).setText("{}".format( msg.get('Mac', "n/a"))) elif reply == "STATUS6": msg = msg['StatusMQT'] self.mqtt_model.item(0, 1).setText("{}".format( msg.get('MqttHost', "n/a"))) self.mqtt_model.item(1, 1).setText("{}".format( msg.get('MqttPort', "n/a"))) self.mqtt_model.item(2, 1).setText("{}".format( msg.get('MqttUser', "n/a"))) self.mqtt_model.item(3, 1).setText("{}".format( msg.get('MqttClient', "n/a"))) self.mqtt_model.item(6, 1).setText("{}".format( self.full_topic)) self.mqtt_model.item(7, 1).setText("cmnd/{}_fb".format( msg.get('MqttClient', "n/a"))) elif reply == "STATUS7": msg = msg['StatusTIM'] self._sunrise = msg.get('Sunrise', "") self._sunset = msg.get('Sunset', "") self.TimerMode.button(1).setText( self.TimerMode.button(1).text().format( msg.get('Sunrise', ""))) self.TimerMode.button(2).setText( self.TimerMode.button(2).text().format( msg.get('Sunset', ""))) except JSONDecodeError: pass def initial_query(self): self.mqtt.publish(self.cmnd_topic + "status", 0) self.mqtt.publish(self.cmnd_topic + "timers") self.mqtt.publish(self.cmnd_topic + "modules") self.mqtt.publish(self.cmnd_topic + "module") self.mqtt.publish(self.cmnd_topic + "gpios") self.mqtt.publish(self.cmnd_topic + "gpio") def parseModules(self, msg): k = list(msg)[0] v = msg[k] if k == "Modules1": self.modules = v else: self.modules += v def updateCBModules(self): self.cbModule.clear() self.cbModule.addItems(self.modules) self.cbModule.setCurrentText(self.module) def saveModule(self): module = self.cbModule.currentText().split(" ")[0] self.mqtt.publish(self.cmnd_topic + "module", payload=module) self.parent().close() def parse_available_gpio(self, msg): self.gpios = list(msg) for i, g in enumerate(self.gpios): cb = QComboBox() cb.addItems(self.supported_gpios) cb.setCurrentText(msg[g]) self.gpio_cb.append(cb) self.gbGPIO.layout().insertRow(0 + i, g, cb) def parse_supported_peripherals(self, msg): k = list(msg)[0] v = msg[k] if k == "GPIOs1": self.supported_gpios = v else: self.supported_gpios += v def saveGPIOs(self): payload = "" for i, g in enumerate(list(self.gpios)): gpio = self.gpio_cb[i].currentText().split(" ")[0] payload += "{} {}; ".format(g, gpio) self.mqtt.publish(self.cmnd_topic + "backlog", payload=payload) self.parent().close() def loadRule(self, idx): self.mqtt.publish(self.cmnd_topic + "Rule{}".format(idx + 1)) def parseRule(self, msg): rule, once, stop, _, rules = list(msg) self.rg.cbEnabled.setChecked(msg[rule] == "ON") self.rg.cbOnce.setChecked(msg[once] == "ON") self.rg.cbStopOnError.setChecked(msg[stop] == "ON") self.rg.text.setPlainText(msg['Rules'].replace( " on ", "\non ").replace(" do ", " do\n\t").replace(" endon", "\nendon ")) def saveRule(self): text = self.rg.text.toPlainText().replace("\n", " ").replace( "\t", " ").replace(" ", " ") backlog = { 'rule_nr': "Rule{}".format(self.rg.cbRule.currentIndex() + 1), 'text': text if len(text) > 0 else '""', 'enabled': "1" if self.rg.cbEnabled.isChecked() else "0", 'once': "5" if self.rg.cbOnce.isChecked() else "4", 'stop': "9" if self.rg.cbStopOnError.isChecked() else "8" } self.mqtt.publish( self.cmnd_topic + "backlog", payload= '{rule_nr} {text}; {rule_nr} {once}; {rule_nr} {stop}; {rule_nr} {enabled}; ' .format(**backlog)) def loadRuleTimers(self): self.mqtt.publish(self.cmnd_topic + "ruletimer") def parseRuleTimer(self, msg): for c in range(8): itm = self.twRT.cellWidget(0, c) itm.setValue(int(msg["T{}".format(c + 1)])) def saveRuleTimers(self): for t in range(8): self.mqtt.publish(self.cmnd_topic + "ruletimer{}".format(t + 1), payload=self.twRT.cellWidget(0, t).value()) def loadVarMem(self): for x in range(5): self.mqtt.publish(self.cmnd_topic + "var{}".format(x + 1)) self.mqtt.publish(self.cmnd_topic + "mem{}".format(x + 1)) def parseVarMem(self, msg, row): k = list(msg)[0] nr = k[-1] v = msg[k] itm = self.twVM.cellWidget(row, int(nr) - 1) itm.setText(v) def saveVarMem(self): for r, cmd in enumerate(['Var', 'Mem']): for c in range(5): self.mqtt.publish(self.cmnd_topic + "{}{}".format(cmd, c + 1), payload="{}".format( self.twVM.cellWidget(r, c).text())) def toggleTimers(self, state): self.mqtt.publish(self.cmnd_topic + "timers", payload="ON" if state else "OFF") def loadTimer(self, idx): self.mqtt.publish(self.cmnd_topic + "Timers") self.mqtt.publish(self.cmnd_topic + "Timer{}".format(idx + 1)) def parseTimer(self, payload): self.blockSignals(True) self.cbTimerArm.setChecked(payload['Arm']) self.cbTimerRpt.setChecked(payload['Repeat']) self.cbxTimerAction.setCurrentIndex(payload['Action']) output = payload.get('Output') if output: self.cbxTimerOut.setEnabled(True) self.cbxTimerOut.setCurrentIndex(output - 1) else: self.cbxTimerOut.setEnabled(False) mode = payload.get('Mode') if not mode: mode = 0 self.TimerMode.button(1).setEnabled(False) self.TimerMode.button(2).setEnabled(False) self.TimerMode.button(mode).setChecked(True) h, m = map(int, payload["Time"].split(":")) if h < 0: self.cbxTimerPM.setCurrentText("-") h *= -1 self.teTimerTime.setTime(QTime(h, m)) self.cbxTimerWnd.setCurrentText(str(payload['Window']).zfill(2)) for wd, v in enumerate(payload['Days']): self.TimerWeekday.button(wd).setChecked(int(v)) self.describeTimer() self.blockSignals(False) def saveTimer(self): payload = { "Arm": int(self.cbTimerArm.isChecked()), "Mode": self.TimerMode.checkedId(), "Time": self.teTimerTime.time().toString("hh:mm"), "Window": self.cbxTimerWnd.currentIndex(), "Days": "".join([ str(int(cb.isChecked())) for cb in self.TimerWeekday.buttons() ]), "Repeat": int(self.cbTimerRpt.isChecked()), "Output": self.cbxTimerOut.currentIndex(), "Action": self.cbxTimerAction.currentIndex() } self.mqtt.publish(self.cmnd_topic + "timer{}".format(self.cbTimer.currentIndex() + 1), payload=dumps(payload)) def copyTrigger(self): mode = self.cbxTimerAction.currentText() if mode == "Rule": trigger = "clock#Timer={}".format(self.cbTimer.currentIndex() + 1) else: trigger = "{}#state={}".format(self.cbxTimerOut.currentText(), self.cbxTimerAction.currentIndex()) QApplication.clipboard().setText("on {} do\n\t\nendon".format(trigger)) def describeTimer(self): if self.cbTimerArm.isChecked(): desc = {'days': '', 'repeat': ''} desc['timer'] = self.cbTimer.currentText().upper() repeat = self.cbTimerRpt.isChecked() out = self.cbxTimerOut.currentText() act = self.cbxTimerAction.currentText() mode = self.TimerMode.checkedId() pm = self.cbxTimerPM.currentText() time = self.teTimerTime.time() wnd = int(self.cbxTimerWnd.currentText()) * 60 if mode == 0: if wnd == 0: desc['time'] = "at {}".format(time.toString("hh:mm")) else: desc['time'] = "somewhere between {} and {}".format( time.addSecs(wnd * -1).toString("hh:mm"), time.addSecs(wnd).toString("hh:mm")) else: prefix = "before" if pm == "-" else "after" mode_desc = "sunrise" if mode == 1 else "sunset" window = "somewhere in a {} minute window centered around ".format( wnd // 30) desc['time'] = "{}h{}m {} {}".format(time.hour(), time.minute(), prefix, mode_desc) if wnd > 0: desc['time'] = window + desc['time'] if repeat: day_names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] days = [cb.isChecked() for cb in self.TimerWeekday.buttons()] if days.count(True) == 7: desc['days'] = "everyday" else: days_list = [day_names[d] for d in range(7) if days[d]] desc['days'] = "on every {}".format(", ".join(days_list)) else: desc['repeat'] = "only ONCE" if act == "Rule": desc['action'] = "trigger clock#Timer={}".format( self.cbTimer.currentIndex() + 1) text = "{timer} will {action} {time} {days} {repeat}".format( **desc) elif self.cbxTimerOut.count() > 0: if act == "Toggle": desc['action'] = "TOGGLE {}".format(out.upper()) else: desc['action'] = "set {} to {}".format( out.upper(), act.upper()) text = "{timer} will {action} {time} {days} {repeat}".format( **desc) else: text = "{timer} will do nothing because there are no relays configured.".format( **desc) self.lbTimerDesc.setText(text) else: self.lbTimerDesc.setText( "{} is not armed, it will do nothing.".format( self.cbTimer.currentText().upper())) def tabInformation(self): info = QWidget() vl = VLayout() self.program_model = QStandardItemModel() for d in [ "Program version", "Build date & time", "Core/SDK version", "Flash write count", "Boot count", "Restart reason", "Friendly Name 1", "Friendly Name 2", "Friendly Name 3", "Friendly Name 4" ]: k = QStandardItem(d) k.setEditable(False) v = QStandardItem() v.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight) v.setEditable(False) self.program_model.appendRow([k, v]) gbPrgm = GroupBoxH("Program") gbPrgm.setFlat(True) tvPrgm = QTreeView() tvPrgm.setHeaderHidden(True) tvPrgm.setRootIsDecorated(False) tvPrgm.setModel(self.program_model) tvPrgm.resizeColumnToContents(0) gbPrgm.addWidget(tvPrgm) self.esp_model = QStandardItemModel() for d in [ "ESP Chip Id", "Flash Chip Id", "Flash Size", "Program Flash Size", "Program Size", "Free Program Space", "Free Memory" ]: k = QStandardItem(d) k.setEditable(False) v = QStandardItem() v.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight) v.setEditable(False) self.esp_model.appendRow([k, v]) gbESP = GroupBoxH("ESP") gbESP.setFlat(True) tvESP = QTreeView() tvESP.setHeaderHidden(True) tvESP.setRootIsDecorated(False) tvESP.setModel(self.esp_model) tvESP.resizeColumnToContents(0) gbESP.addWidget(tvESP) # self.emul_model = QStandardItemModel() # for d in ["Emulation", "mDNS Discovery"]: # k = QStandardItem(d) # k.setEditable(False) # v = QStandardItem() # v.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight) # v.setEditable(False) # self.emul_model.appendRow([k, v]) # # gbEmul = GroupBoxH("Emulation") # gbEmul.setFlat(True) # tvEmul = QTreeView() # tvEmul.setHeaderHidden(True) # tvEmul.setRootIsDecorated(False) # tvEmul.setModel(self.emul_model) # tvEmul.resizeColumnToContents(0) # gbEmul.addWidget(tvEmul) self.wifi_model = QStandardItemModel() for d in [ "AP1 SSId (RSSI)", "Hostname", "IP Address", "Gateway", "Subnet Mask", "DNS Server", "MAC Address" ]: k = QStandardItem(d) k.setEditable(False) v = QStandardItem() v.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight) v.setEditable(False) self.wifi_model.appendRow([k, v]) gbWifi = GroupBoxH("Wifi") gbWifi.setFlat(True) tvWifi = QTreeView() tvWifi.setHeaderHidden(True) tvWifi.setRootIsDecorated(False) tvWifi.setModel(self.wifi_model) tvWifi.resizeColumnToContents(0) gbWifi.addWidget(tvWifi) self.mqtt_model = QStandardItemModel() for d in [ "MQTT Host", "MQTT Port", "MQTT User", "MQTT Client", "MQTT Topic", "MQTT Group Topic", "MQTT Full Topic", "MQTT Fallback Topic" ]: k = QStandardItem(d) k.setEditable(False) v = QStandardItem() v.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight) v.setEditable(False) self.mqtt_model.appendRow([k, v]) gbMQTT = GroupBoxH("MQTT") gbMQTT.setFlat(True) tvMQTT = QTreeView() tvMQTT.setHeaderHidden(True) tvMQTT.setRootIsDecorated(False) tvMQTT.setModel(self.mqtt_model) tvMQTT.resizeColumnToContents(0) gbMQTT.addWidget(tvMQTT) hl = HLayout(0) vl_lc = VLayout(0, 3) vl_rc = VLayout(0, 3) vl_lc.addWidgets([gbPrgm, gbESP]) vl_rc.addWidgets([gbWifi, gbMQTT]) vl_rc.setStretch(0, 2) vl_rc.setStretch(1, 2) vl_rc.setStretch(2, 1) hl.addLayout(vl_lc) hl.addLayout(vl_rc) vl.addLayout(hl) info.setLayout(vl) return info def tabModule(self): module = QWidget() module.setLayout(HLayout()) self.gbModule = GroupBoxH("Module") self.cbModule = QComboBox() self.pbModuleSet = QPushButton("Save and close (device will restart)") self.gbModule.addWidgets([self.cbModule, self.pbModuleSet]) self.pbModuleSet.clicked.connect(self.saveModule) self.gbGPIO = QGroupBox("GPIO") fl_gpio = QFormLayout() pbGPIOSet = QPushButton("Save and close (device will restart)") fl_gpio.addWidget(pbGPIOSet) pbGPIOSet.clicked.connect(self.saveGPIOs) self.gbGPIO.setLayout(fl_gpio) mg_vl = VLayout([0, 0, 3, 0]) mg_vl.addWidgets([self.gbModule, self.gbGPIO]) mg_vl.setStretch(0, 1) mg_vl.setStretch(1, 3) self.gbFirmware = GroupBoxV("Firmware", margin=[3, 0, 0, 0]) lb = QLabel("Feature under development.") lb.setAlignment(Qt.AlignCenter) lb.setEnabled(False) self.gbFirmware.addWidget(lb) module.layout().addLayout(mg_vl) module.layout().addWidget(self.gbFirmware) return module def tabRules(self): rules = QWidget() rules.setLayout(VLayout()) hl = HLayout(0) vl_l = VLayout(0) self.rg = RuleGroupBox(rules, "Rule editor") self.rg.setFlat(True) self.rg.cbRule.currentIndexChanged.connect(self.loadRule) vl_l.addWidget(self.rg) gRT = GroupBoxH("Rule timers") vl_RT_func = VLayout(margin=[0, 0, 3, 0]) self.pbRTPoll = QPushButton("Poll") self.pbRTPoll.setCheckable(True) self.pbRTSet = QPushButton("Set") vl_RT_func.addWidgets([self.pbRTPoll, self.pbRTSet]) vl_RT_func.addStretch(1) gRT.layout().addLayout(vl_RT_func) self.twRT = QTableWidget(1, 8) self.twRT.setHorizontalHeaderLabels( ["T{}".format(i) for i in range(1, 9)]) for c in range(8): self.twRT.horizontalHeader().setSectionResizeMode( c, QHeaderView.Stretch) self.twRT.setCellWidget(0, c, SpinBox(minimum=0, maximum=32766)) self.twRT.verticalHeader().hide() self.twRT.verticalHeader().setDefaultSectionSize( self.twRT.horizontalHeader().height() * 2 + 1) self.twRT.setMaximumHeight(self.twRT.horizontalHeader().height() + self.twRT.rowHeight(0)) gRT.layout().addWidget(self.twRT) gVM = GroupBoxH("VAR/MEM") vl_VM_func = VLayout(margin=[3, 0, 0, 0]) self.pbVMPoll = QPushButton("Poll") self.pbVMPoll.setCheckable(True) self.pbVMSet = QPushButton("Set") vl_VM_func.addWidgets([self.pbVMPoll, self.pbVMSet]) vl_VM_func.addStretch(1) gVM.layout().addLayout(vl_VM_func) self.twVM = QTableWidget(2, 5) self.twVM.setHorizontalHeaderLabels( ["{}".format(i) for i in range(1, 9)]) self.twVM.setVerticalHeaderLabels(["VAR", "MEM"]) self.twVM.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) for c in range(5): self.twVM.horizontalHeader().setSectionResizeMode( c, QHeaderView.Stretch) for r in range(2): for c in range(5): self.twVM.setCellWidget(r, c, QLineEdit()) self.twVM.verticalHeader().setDefaultSectionSize( self.twVM.horizontalHeader().height()) self.twVM.setMaximumHeight(self.twVM.horizontalHeader().height() + self.twVM.rowHeight(0) * 2) gVM.layout().addWidget(self.twVM) hl_rt_vm = HLayout(0) hl_rt_vm.addWidgets([gRT, gVM]) hl.addLayout(vl_l) vl_r = VLayout(0) self.gbTimers = GroupBoxV("Timers", spacing=5) self.gbTimers.setCheckable(True) self.gbTimers.setChecked(False) self.gbTimers.toggled.connect(self.toggleTimers) self.cbTimer = QComboBox() self.cbTimer.addItems(["Timer{}".format(nr + 1) for nr in range(16)]) self.cbTimer.currentIndexChanged.connect(self.loadTimer) hl_tmr_arm_rpt = HLayout(0) self.cbTimerArm = QCheckBox("Arm") self.cbTimerArm.clicked.connect(lambda x: self.describeTimer()) self.cbTimerRpt = QCheckBox("Repeat") self.cbTimerRpt.clicked.connect(lambda x: self.describeTimer()) hl_tmr_arm_rpt.addWidgets([self.cbTimerArm, self.cbTimerRpt]) hl_tmr_out_act = HLayout(0) self.cbxTimerOut = QComboBox() self.cbxTimerOut.currentIndexChanged.connect( lambda x: self.describeTimer()) self.cbxTimerAction = QComboBox() self.cbxTimerAction.addItems(["Off", "On", "Toggle", "Rule"]) self.cbxTimerAction.currentIndexChanged.connect( lambda x: self.describeTimer()) hl_tmr_out_act.addWidgets([self.cbxTimerOut, self.cbxTimerAction]) self.TimerMode = QButtonGroup() rbTime = QRadioButton("Time") rbSunrise = QRadioButton("Sunrise ({})") rbSunset = QRadioButton("Sunset ({})") self.TimerMode.addButton(rbTime, 0) self.TimerMode.addButton(rbSunrise, 1) self.TimerMode.addButton(rbSunset, 2) self.TimerMode.buttonClicked.connect(lambda x: self.describeTimer()) gbTimerMode = GroupBoxH("Mode") gbTimerMode.addWidgets(self.TimerMode.buttons()) hl_tmr_time = HLayout(0) self.cbxTimerPM = QComboBox() self.cbxTimerPM.addItems(["+", "-"]) self.cbxTimerPM.currentIndexChanged.connect( lambda x: self.describeTimer()) self.TimerMode.buttonClicked[int].connect( lambda x: self.cbxTimerPM.setEnabled(x != 0)) self.teTimerTime = QTimeEdit() self.teTimerTime.setButtonSymbols(QTimeEdit.NoButtons) self.teTimerTime.setAlignment(Qt.AlignCenter) self.teTimerTime.timeChanged.connect(lambda x: self.describeTimer()) lbWnd = QLabel("Window:") lbWnd.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.cbxTimerWnd = QComboBox() self.cbxTimerWnd.addItems([str(x).zfill(2) for x in range(0, 16)]) self.cbxTimerWnd.currentIndexChanged.connect( lambda x: self.describeTimer()) hl_tmr_days = HLayout(0) self.TimerWeekday = QButtonGroup() self.TimerWeekday.setExclusive(False) for i, wd in enumerate( ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]): cb = QCheckBox(wd) cb.clicked.connect(lambda x: self.describeTimer()) hl_tmr_days.addWidget(cb) self.TimerWeekday.addButton(cb, i) gbTimerDesc = GroupBoxV("Timer description", 5) gbTimerDesc.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) self.lbTimerDesc = QLabel() self.lbTimerDesc.setAlignment(Qt.AlignCenter) self.lbTimerDesc.setWordWrap(True) gbTimerDesc.layout().addWidget(self.lbTimerDesc) hl_tmr_btns = HLayout(0) btnCopyTrigger = QPushButton("Copy trigger") btnTimerSave = QPushButton("Save") hl_tmr_btns.addWidgets([btnCopyTrigger, btnTimerSave]) hl_tmr_btns.insertStretch(1) btnTimerSave.clicked.connect(self.saveTimer) btnCopyTrigger.clicked.connect(self.copyTrigger) hl_tmr_time.addWidgets( [self.cbxTimerPM, self.teTimerTime, lbWnd, self.cbxTimerWnd]) self.gbTimers.layout().addWidget(self.cbTimer) self.gbTimers.layout().addLayout(hl_tmr_arm_rpt) self.gbTimers.layout().addLayout(hl_tmr_out_act) self.gbTimers.layout().addWidget(gbTimerMode) self.gbTimers.layout().addLayout(hl_tmr_time) self.gbTimers.layout().addLayout(hl_tmr_days) self.gbTimers.layout().addWidget(gbTimerDesc) self.gbTimers.layout().addLayout(hl_tmr_btns) vl_r.addWidget(self.gbTimers) hl.addLayout(vl_r) hl.setStretch(0, 2) hl.setStretch(1, 1) rules.layout().addLayout(hl) rules.layout().addLayout(hl_rt_vm) rules.layout().setStretch(0, 3) rules.layout().setStretch(1, 0) return rules def closeEvent(self, event): self.mqtt.disconnectFromHost() event.accept()
class DevicesConfigWidget(QWidget): def __init__(self, parent, topic, *args, **kwargs): super(DevicesConfigWidget, self).__init__(*args, **kwargs) self.settings = QSettings() self.topic = topic self.full_topic = self.settings.value( 'Devices/{}/full_topic'.format(topic)) self.friendly_name = self.settings.value( 'Devices/{}/friendly_name'.format(topic)) self.cmnd_topic = self.full_topic.replace("%topic%", self.topic).replace( "%prefix%", 'cmnd') self.tele_topic = self.full_topic.replace( "%topic%", self.topic).replace("%prefix%", 'tele') + "+" self.stat_topic = self.full_topic.replace( "%topic%", self.topic).replace("%prefix%", 'stat') + "+" self.setWindowTitle(self.friendly_name) self.mqtt = MqttClient() self.module = None self.modules = [] self.gpios = [] self.supported_gpios = [] self.current_gpios = {} self.setLayout(VLayout(margin=[0, 6, 0, 0], spacing=3)) self.build_detail_row() self.build_tabs() self.create_timers() def create_timers(self): self.auto_timer = QTimer() self.auto_timer.setInterval(1000) self.auto_timer.timeout.connect(self.auto) self.auto_timer.start() self.mqtt_timer = QTimer() self.mqtt_timer.setSingleShot(True) self.mqtt_timer.timeout.connect(self.setupMqtt) self.mqtt_timer.start(1000) def build_tabs(self): self.tabs = QTabWidget() self.rule_grps = [] tabModule = self.tabModule() self.tabs.addTab(tabModule, "Module | Firmware") tabRules = self.tabRules() self.tabs.addTab(tabRules, "Rules") self.pbRTSet.clicked.connect(self.saveRuleTimers) self.pbVMSet.clicked.connect(self.saveVarMem) for r in range(3): self.rule_grps[r].pbSave.clicked.connect( lambda x, r=r: self.saveRule(r)) self.tabs.currentChanged.connect(self.tabChanged) self.tabs.setEnabled(False) self.layout().addWidget(self.tabs) def build_detail_row(self): frDetails = QFrame() frDetails.setFrameStyle(QFrame.StyledPanel | QFrame.Plain) hl_details = HLayout() leTopic = DetailLE(self.topic) hl_details.addWidgets([QLabel("Topic"), leTopic]) leFTopic = DetailLE(self.full_topic) hl_details.addWidgets([QLabel("FullTopic"), leFTopic]) # self.leMAC = DetailLE("") # hl_details.addWidgets([QLabel("MAC"), self.leMAC]) # self.leIP = DetailLE("") # hl_details.addWidgets([QLabel("IP"), self.leIP]) self.leGTopic = DetailLE("") hl_details.addWidgets([QLabel("GroupTopic"), self.leGTopic]) frDetails.setLayout(hl_details) self.layout().addWidget(frDetails) def auto(self): if self.mqtt.state == self.mqtt.Connected: if self.pbRTPoll.isChecked(): self.loadRuleTimers() if self.pbVMPoll.isChecked(): self.loadVarMem() def tabChanged(self, tab): if tab == 1: self.loadRule() self.loadRuleTimers() self.loadVarMem() def torture(self): # self.mqtt.publish(self.cmnd_topic + "status", payload="0") print('x') def setupMqtt(self): self.mqtt.hostname = self.settings.value('hostname', 'localhost') self.mqtt.port = self.settings.value('port', 1883, int) if self.settings.value('username'): self.mqtt.setAuth(self.settings.value('username'), self.settings.value('password')) self.mqtt.connectToHost() self.mqtt.connected.connect(self.mqtt_subscribe) self.mqtt.messageSignal.connect(self.mqtt_message) def mqtt_subscribe(self): self.mqtt.subscribe(self.tele_topic) self.mqtt.subscribe(self.stat_topic) self.initial_query() self.tabs.setEnabled(True) def mqtt_message(self, topic, msg): match = match_topic(self.full_topic, topic) if match: match = match.groupdict() reply = match['reply'] if reply == "RESULT": msg = loads(msg) first = list(msg)[0] if first.startswith('Rule'): self.parseRule(msg) elif first == "T1": self.parseRuleTimer(msg) elif first.startswith('Var'): self.parseVarMem(msg, 0) elif first.startswith('Mem'): self.parseVarMem(msg, 1) elif first == "Module": self.module = msg[first] elif first.startswith('Modules'): self.parseModules(msg) elif first.startswith( 'GPIO') and not first.startswith('GPIOs'): self.parseGPIO(msg) elif first.startswith('GPIOs'): self.parseGPIOs(msg) else: print(msg) elif reply == "STATUS1": msg = loads(msg)['StatusPRM'] self.leGTopic.setText(msg.get("GroupTopic")) def initial_query(self): self.mqtt.publish(self.cmnd_topic + "module") self.mqtt.publish(self.cmnd_topic + "modules") self.mqtt.publish(self.cmnd_topic + "gpio") self.mqtt.publish(self.cmnd_topic + "gpios") self.mqtt.publish(self.cmnd_topic + "status", 1) def parseModules(self, msg): k = list(msg)[0] nr = k[-1] v = msg[k] if k == "Modules1": self.modules = v elif k == "Modules2": self.modules += v elif k == "Modules3": self.modules += v self.updateCBModules() def updateCBModules(self): self.cbModule.addItems(self.modules) self.cbModule.setCurrentText(self.module) def updateCBGpios(self): for i, cb in enumerate(self.gpios): cb.addItems(self.supported_gpios) cb.setCurrentText(self.current_gpios[list(self.current_gpios)[i]]) def saveModule(self): module = self.cbModule.currentText().split(" ")[0] self.mqtt.publish(self.cmnd_topic + "module", payload=module) self.parent().close() def parseGPIO(self, msg): for g in list(msg): self.current_gpios[g] = (msg[g]) cb = QComboBox() self.gpios.append(cb) self.gbGPIO.layout().addRow(QLabel(g), cb) pbGPIOSet = QPushButton("Save and close (device will restart)") pbGPIOSet.clicked.connect(self.saveGPIOs) self.gbGPIO.layout().addWidget(pbGPIOSet) def parseGPIOs(self, msg): k = list(msg)[0] nr = k[-1] v = msg[k] if k == "GPIOs1": self.supported_gpios = v elif k == "GPIOs2": self.supported_gpios += v elif k == "GPIOs3": self.supported_gpios += v self.updateCBGpios() def saveGPIOs(self): payload = "" for i, g in enumerate(list(self.current_gpios)): gpio = self.gpios[i].currentText().split(" ")[0] payload += "{} {}; ".format(g, gpio) self.mqtt.publish(self.cmnd_topic + "backlog", payload=payload) self.parent().close() def loadRule(self, rule=None): if rule: self.mqtt.publish(self.cmnd_topic + "Rule{}".format(rule)) else: for r in range(1, 4): self.mqtt.publish(self.cmnd_topic + "Rule{}".format(r)) def parseRule(self, msg): rule, once, stop, _, rules = list(msg) rg = self.rule_grps[int(rule[-1]) - 1] rg.cbEnabled.setChecked(msg[rule] == "ON") rg.text.setPlainText(msg['Rules'].replace(" on ", "\non ").replace( " do ", " do\n\t").replace(" endon", "\nendon ")) def saveRule(self, rule): rg = self.rule_grps[rule] text = rg.text.toPlainText().replace("\n", " ").replace("\t", " ").replace( " ", " ") self.mqtt.publish(self.cmnd_topic + "Rule{}".format(rule + 1), payload=text) backlog = { 'rule_nr': "Rule{}".format(rule + 1), 'enabled': "1" if rg.cbEnabled.isChecked() else "0", 'once': "5" if rg.cbOnce.isChecked() else "4", 'stop': "9" if rg.cbStopOnError.isChecked() else "8" } self.mqtt.publish( self.cmnd_topic + "backlog", payload="{rule_nr} {once}; {rule_nr} {stop}; {rule_nr} {enabled}; " .format(**backlog)) def loadRuleTimers(self): self.mqtt.publish(self.cmnd_topic + "ruletimer") def parseRuleTimer(self, msg): for c in range(8): itm = self.twRT.cellWidget(0, c) itm.setValue(int(msg["T{}".format(c + 1)])) def saveRuleTimers(self): for t in range(8): self.mqtt.publish(self.cmnd_topic + "ruletimer{}".format(t + 1), payload=self.twRT.cellWidget(0, t).value()) def loadVarMem(self): for x in range(5): self.mqtt.publish(self.cmnd_topic + "var{}".format(x + 1)) self.mqtt.publish(self.cmnd_topic + "mem{}".format(x + 1)) def parseVarMem(self, msg, row): k = list(msg)[0] nr = k[-1] v = msg[k] itm = self.twVM.cellWidget(row, int(nr) - 1) itm.setText(v) def saveVarMem(self): for r, cmd in enumerate(['Var', 'Mem']): for c in range(5): self.mqtt.publish(self.cmnd_topic + "{}{}".format(cmd, c + 1), payload=self.twVM.cellWidget(r, c).text()) def tabModule(self): module = QWidget() module.setLayout(HLayout()) self.gbModule = QGroupBox("Module") fl_module = QFormLayout() self.cbModule = QComboBox() fl_module.addRow("Module type", self.cbModule) self.pbModuleSet = QPushButton("Save and close (device will restart)") self.pbModuleSet.clicked.connect(self.saveModule) fl_module.addWidget(self.pbModuleSet) self.gbModule.setLayout(fl_module) self.gbGPIO = QGroupBox("GPIO") fl_gpio = QFormLayout() self.gbGPIO.setLayout(fl_gpio) mg_vl = VLayout([0, 0, 3, 0]) mg_vl.addWidgets([self.gbModule, self.gbGPIO]) mg_vl.setStretch(0, 1) mg_vl.setStretch(1, 3) self.gbFirmware = GroupBoxV("Firmware", margin=[3, 0, 0, 0]) lb = QLabel("Feature under development.") lb.setAlignment(Qt.AlignCenter) lb.setEnabled(False) self.gbFirmware.addWidget(lb) module.layout().addLayout(mg_vl) module.layout().addWidget(self.gbFirmware) return module def tabRules(self): rules = QWidget() rules.setLayout(VLayout()) for r in range(3): rg = RuleGroupBox(rules, "Rule buffer {}".format(r + 1)) rg.pbLoad.clicked.connect(lambda x, r=r + 1: self.loadRule(r)) self.rule_grps.append(rg) rules.layout().addWidget(rg) rules.layout().setStretch(r, 1) gRT = GroupBoxH("Rule timers") vl_RT_func = VLayout(margin=[0, 0, 3, 0]) self.pbRTPoll = QPushButton("Poll") self.pbRTPoll.setCheckable(True) self.pbRTSet = QPushButton("Set") vl_RT_func.addWidgets([self.pbRTPoll, self.pbRTSet]) vl_RT_func.addStretch(1) gRT.layout().addLayout(vl_RT_func) self.twRT = QTableWidget(1, 8) self.twRT.setHorizontalHeaderLabels( ["T{}".format(i) for i in range(1, 9)]) for c in range(8): self.twRT.horizontalHeader().setSectionResizeMode( c, QHeaderView.Stretch) self.twRT.setCellWidget(0, c, SpinBox(minimum=0, maximum=32766)) self.twRT.verticalHeader().hide() self.twRT.verticalHeader().setDefaultSectionSize( self.twRT.horizontalHeader().height() * 2 + 1) self.twRT.setMaximumHeight(self.twRT.horizontalHeader().height() + self.twRT.rowHeight(0)) gRT.layout().addWidget(self.twRT) gVM = GroupBoxH("VAR/MEM") vl_VM_func = VLayout(margin=[3, 0, 0, 0]) self.pbVMPoll = QPushButton("Poll") self.pbVMPoll.setCheckable(True) self.pbVMSet = QPushButton("Set") vl_VM_func.addWidgets([self.pbVMPoll, self.pbVMSet]) vl_VM_func.addStretch(1) gVM.layout().addLayout(vl_VM_func) self.twVM = QTableWidget(2, 5) self.twVM.setHorizontalHeaderLabels( ["{}".format(i) for i in range(1, 9)]) self.twVM.setVerticalHeaderLabels(["VAR", "MEM"]) self.twVM.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) for c in range(5): self.twVM.horizontalHeader().setSectionResizeMode( c, QHeaderView.Stretch) for r in range(2): for c in range(5): self.twVM.setCellWidget(r, c, QLineEdit()) self.twVM.verticalHeader().setDefaultSectionSize( self.twVM.horizontalHeader().height()) self.twVM.setMaximumHeight(self.twVM.horizontalHeader().height() + self.twVM.rowHeight(0) * 2) gVM.layout().addWidget(self.twVM) hl_rt_vm = HLayout() hl_rt_vm.addWidgets([gRT, gVM]) rules.layout().addLayout(hl_rt_vm) rules.layout().setStretch(3, 0) return rules def closeEvent(self, event): self.mqtt.disconnectFromHost() event.accept()