def hostsMenu(self, menu): global Settings global Hosts global Translations if (len(Hosts) <= 0): return False # hostMenu = menu.addMenu(Translations["Menu::Machines"]) hostGroup = QActionGroup(hostMenu) hostGroup.setEnabled(True) hostGroup.setExclusive(False) hostGroup.triggered.connect(self.doHostTriggered) # KEYs = Hosts.keys() for H in KEYs: hostAction = QAction(H, hostMenu) hostAction.setData(H) hostMenu.addAction(hostAction) hostGroup.addAction(hostAction) # self.Actions["Machines"] = hostMenu self.Actions["MachinesGroup"] = hostGroup return True
class PangoToolBarWidget(QToolBar): del_labels_signal = pyqtSignal(int) def __init__(self, parent=None): super().__init__(parent) self.setIconSize(QSize(16, 16)) self.scene = None spacer_left = QWidget() spacer_left.setFixedWidth(10) spacer_middle = QWidget() spacer_middle.setFixedWidth(50) spacer_right = QWidget() spacer_right.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) # Label Related self.color_display = QLabel() self.color_display.setFixedSize(QSize(50, 20)) self.label_select = self.LabelSelect(self.color_display) self.label_select.lineEdit().returnPressed.connect(self.add) self.add_action = QAction("Add") self.add_action.triggered.connect(self.add) icon = pango_get_icon("add") self.add_action.setIcon(icon) self.del_action = QAction("Delete") self.del_action.triggered.connect(self.delete) icon = pango_get_icon("del") self.del_action.setIcon(icon) self.color_action = QAction("Palette") self.color_action.triggered.connect(self.set_color) icon = pango_get_icon("palette") self.color_action.setIcon(icon) # Tool Related self.size_select = QSpinBox() self.size_select.valueChanged.connect(self.set_tool_size) self.size_select.setSuffix("px") self.size_select.setRange(1, 99) self.size_select.setSingleStep(5) self.size_select.setValue(10) self.pan_action = QAction("Pan") self.lasso_action = QAction("Lasso") self.path_action = QAction("Path") self.bbox_action = QAction("Bbox") self.poly_action = QAction("Poly") self.action_group = QActionGroup(self) self.action_group.setExclusive(True) self.action_group.triggered.connect(self.set_tool) self.action_group.addAction(self.pan_action) self.action_group.addAction(self.lasso_action) self.action_group.addAction(self.bbox_action) self.action_group.addAction(self.poly_action) self.action_group.addAction(self.path_action) for action in self.action_group.actions(): icon = pango_get_icon(action.text()) action.setIcon(icon) action.setCheckable(True) # Other font = QFont("Arial", 10) self.info_display = QLabel() self.info_display.setFixedWidth(100) self.info_display.setFont(font) self.coord_display = QLabel() self.coord_display.setFixedWidth(40) self.coord_display.setFont(font) # Layouts self.addWidget(spacer_left) self.addWidget(self.color_display) self.addWidget(self.label_select) self.addAction(self.add_action) self.addAction(self.del_action) self.addAction(self.color_action) self.addWidget(spacer_middle) self.addActions(self.action_group.actions()) self.addWidget(self.size_select) self.addWidget(spacer_right) self.addWidget(self.info_display) self.addWidget(self.coord_display) self.size_select.setEnabled(False) self.del_action.setEnabled(False) self.action_group.setEnabled(False) self.color_action.setEnabled(False) def set_color(self): dialog = QColorDialog() dialog.setOption(QColorDialog.ShowAlphaChannel, False) color = dialog.getColor() if color == QColor(): return row = self.label_select.currentIndex() label = self.label_select.model().item(row, 0) label.color = color label.set_icon() for i in range(0, label.rowCount()): shape = label.child(i) shape.set_icon() # Refresh label (for scene reticle etc.) self.label_select.setCurrentIndex(0) self.label_select.setCurrentIndex(row) self.label_select.color_display.update() def add(self): self.del_action.setEnabled(True) self.color_action.setEnabled(True) if self.scene.fpath is not None: self.action_group.setEnabled(True) item = PangoLabelItem() root = self.label_select.model().invisibleRootItem() root.appendRow(item) item.name = "Unnamed Label " + str(item.row()) item.visible = True item.color = pango_get_palette(item.row()) item.set_icon() bottom_row = self.label_select.model().rowCount() - 1 self.label_select.setCurrentIndex(bottom_row) if bottom_row == 0: self.label_select.currentIndexChanged.emit( self.label_select.currentIndex()) def delete(self): self.reset_tool() self.del_labels_signal.emit(self.label_select.currentIndex()) if self.label_select.model().rowCount() == 0: self.del_action.setEnabled(False) self.action_group.setEnabled(False) self.color_action.setEnabled(False) def set_tool(self, action): if self.scene is None: return self.scene.tool = action.text() self.scene.reset_com() self.scene.reticle.setVisible(self.scene.tool == "Path") self.scene.views()[0].set_cursor(self.scene.tool) if action.text() == "Path" or action.text() == "Filled Path": self.size_select.setEnabled(True) else: self.size_select.setEnabled(False) def reset_tool(self): self.lasso_action.setChecked(True) self.set_tool(self.lasso_action) def set_tool_size(self, size, additive=False): if self.scene is None: return if additive: self.scene.tool_size += size else: self.scene.tool_size = size self.scene.reset_com() self.scene.reticle.setRect(-size / 2, -size / 2, size, size) if self.size_select.value() != self.scene.tool_size: self.size_select.setValue(self.scene.tool_size) if not self.scene.sceneRect().contains(self.scene.reticle.pos()): view = self.scene.views()[0] x = self.size_select.geometry().center().x() y = view.rect().top() + size / 2 self.scene.reticle.setPos(view.mapToScene(QPoint(x, y))) def set_scene(self, scene): if self.scene is not None: self.scene.clear_tool.disconnect(self.reset_tool) self.scene = scene self.scene.clear_tool.connect(self.reset_tool) self.reset_tool() class LabelSelect(QComboBox): def __init__(self, color_display, parent=None): super().__init__(parent) self.color_display = color_display self.setFixedWidth(150) self.setEditable(True) self.editTextChanged.connect(self.edit_text_changed) def paintEvent(self, event): super().paintEvent(event) item = self.model().item(self.currentIndex()) if item is not None and item.color is not None: self.color_display.setStyleSheet( "QLabel { background-color : " + item.color.name() + "}") else: self.color_display.setStyleSheet( "QLabel { background-color : rgba(255, 255, 255, 10)}") def edit_text_changed(self, text): row = self.currentIndex() self.setItemData(row, text, Qt.DisplayRole) def select_next_label(self): idx = self.currentIndex() + 1 if idx > -1 and idx < self.count(): self.setCurrentIndex(idx) def select_prev_label(self): idx = self.currentIndex() - 1 if idx > -1 and idx < self.count(): self.setCurrentIndex(idx)
class ListWidget(QWidget): deviceSelected = pyqtSignal(TasmotaDevice) openRulesEditor = pyqtSignal() openConsole = pyqtSignal() openTelemetry = pyqtSignal() openWebUI = pyqtSignal() def __init__(self, parent, *args, **kwargs): super(ListWidget, self).__init__(*args, **kwargs) self.setWindowTitle("Devices list") self.setWindowState(Qt.WindowMaximized) self.setLayout(VLayout(margin=0, spacing=0)) self.mqtt = parent.mqtt self.env = parent.env self.device = None self.idx = None self.nam = QNetworkAccessManager() self.backup = bytes() self.settings = QSettings("{}/TDM/tdm.cfg".format(QDir.homePath()), QSettings.IniFormat) views_order = self.settings.value("views_order", []) self.views = {} self.settings.beginGroup("Views") views = self.settings.childKeys() if views and views_order: for view in views_order.split(";"): view_list = self.settings.value(view).split(";") self.views[view] = base_view + view_list else: self.views = default_views self.settings.endGroup() self.tb = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonTextBesideIcon) self.tb_relays = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonIconOnly) # self.tb_filter = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonTextBesideIcon) self.tb_views = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonTextBesideIcon) self.pwm_sliders = [] self.layout().addWidget(self.tb) self.layout().addWidget(self.tb_relays) # self.layout().addWidget(self.tb_filter) self.device_list = TableView() self.device_list.setIconSize(QSize(24, 24)) self.model = parent.device_model self.model.setupColumns(self.views["Home"]) self.sorted_device_model = QSortFilterProxyModel() self.sorted_device_model.setFilterCaseSensitivity(Qt.CaseInsensitive) self.sorted_device_model.setSourceModel(parent.device_model) self.sorted_device_model.setSortRole(Qt.InitialSortOrderRole) self.sorted_device_model.setFilterKeyColumn(-1) self.device_list.setModel(self.sorted_device_model) self.device_list.setupView(self.views["Home"]) self.device_list.setSortingEnabled(True) self.device_list.setWordWrap(True) self.device_list.setItemDelegate(DeviceDelegate()) self.device_list.sortByColumn(self.model.columnIndex("FriendlyName"), Qt.AscendingOrder) self.device_list.setContextMenuPolicy(Qt.CustomContextMenu) self.device_list.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) self.layout().addWidget(self.device_list) self.layout().addWidget(self.tb_views) self.device_list.clicked.connect(self.select_device) self.device_list.customContextMenuRequested.connect(self.show_list_ctx_menu) self.ctx_menu = QMenu() self.ctx_menu_relays = None self.create_actions() self.create_view_buttons() # self.create_view_filter() self.device_list.doubleClicked.connect(lambda: self.openConsole.emit()) def create_actions(self): self.ctx_menu_cfg = QMenu("Configure") self.ctx_menu_cfg.setIcon(QIcon("GUI/icons/settings.png")) self.ctx_menu_cfg.addAction("Module", self.configureModule) self.ctx_menu_cfg.addAction("GPIO", self.configureGPIO) self.ctx_menu_cfg.addAction("Template", self.configureTemplate) # self.ctx_menu_cfg.addAction("Wifi", self.ctx_menu_teleperiod) # self.ctx_menu_cfg.addAction("Time", self.cfgTime.emit) # self.ctx_menu_cfg.addAction("MQTT", self.ctx_menu_teleperiod) # self.ctx_menu_cfg.addAction("Logging", self.ctx_menu_teleperiod) self.ctx_menu.addMenu(self.ctx_menu_cfg) self.ctx_menu.addSeparator() self.ctx_menu.addAction(QIcon("GUI/icons/refresh.png"), "Refresh", self.ctx_menu_refresh) self.ctx_menu.addSeparator() self.ctx_menu.addAction(QIcon("GUI/icons/clear.png"), "Clear retained", self.ctx_menu_clear_retained) self.ctx_menu.addAction("Clear Backlog", self.ctx_menu_clear_backlog) self.ctx_menu.addSeparator() self.ctx_menu.addAction(QIcon("GUI/icons/copy.png"), "Copy", self.ctx_menu_copy) self.ctx_menu.addSeparator() self.ctx_menu.addAction(QIcon("GUI/icons/restart.png"), "Restart", self.ctx_menu_restart) self.ctx_menu.addAction(QIcon(), "Reset", self.ctx_menu_reset) self.ctx_menu.addSeparator() self.ctx_menu.addAction(QIcon("GUI/icons/delete.png"), "Delete", self.ctx_menu_delete_device) console = self.tb.addAction(QIcon("GUI/icons/console.png"), "Console", self.openConsole.emit) console.setShortcut("Ctrl+E") rules = self.tb.addAction(QIcon("GUI/icons/rules.png"), "Rules", self.openRulesEditor.emit) rules.setShortcut("Ctrl+R") self.tb.addAction(QIcon("GUI/icons/timers.png"), "Timers", self.configureTimers) buttons = self.tb.addAction(QIcon("GUI/icons/buttons.png"), "Buttons", self.configureButtons) buttons.setShortcut("Ctrl+B") switches = self.tb.addAction(QIcon("GUI/icons/switches.png"), "Switches", self.configureSwitches) switches.setShortcut("Ctrl+S") power = self.tb.addAction(QIcon("GUI/icons/power.png"), "Power", self.configurePower) power.setShortcut("Ctrl+P") # setopts = self.tb.addAction(QIcon("GUI/icons/setoptions.png"), "SetOptions", self.configureSO) # setopts.setShortcut("Ctrl+S") self.tb.addSpacer() telemetry = self.tb.addAction(QIcon("GUI/icons/telemetry.png"), "Telemetry", self.openTelemetry.emit) telemetry.setShortcut("Ctrl+T") webui = self.tb.addAction(QIcon("GUI/icons/web.png"), "WebUI", self.openWebUI.emit) webui.setShortcut("Ctrl+U") # self.tb.addAction(QIcon(), "Multi Command", self.ctx_menu_webui) self.agAllPower = QActionGroup(self) self.agAllPower.addAction(QIcon("GUI/icons/P_ON.png"), "All ON") self.agAllPower.addAction(QIcon("GUI/icons/P_OFF.png"), "All OFF") self.agAllPower.setEnabled(False) self.agAllPower.setExclusive(False) self.agAllPower.triggered.connect(self.toggle_power_all) self.tb_relays.addActions(self.agAllPower.actions()) self.agRelays = QActionGroup(self) self.agRelays.setVisible(False) self.agRelays.setExclusive(False) for a in range(1, 9): act = QAction(QIcon("GUI/icons/P{}_OFF.png".format(a)), "") act.setShortcut("F{}".format(a)) self.agRelays.addAction(act) self.agRelays.triggered.connect(self.toggle_power) self.tb_relays.addActions(self.agRelays.actions()) self.tb_relays.addSeparator() self.actColor = self.tb_relays.addAction(QIcon("GUI/icons/color.png"), "Color", self.set_color) self.actColor.setEnabled(False) self.actChannels = self.tb_relays.addAction(QIcon("GUI/icons/sliders.png"), "Channels") self.actChannels.setEnabled(False) self.mChannels = QMenu() self.actChannels.setMenu(self.mChannels) self.tb_relays.widgetForAction(self.actChannels).setPopupMode(QToolButton.InstantPopup) def create_view_buttons(self): self.tb_views.addWidget(QLabel("View mode: ")) ag_views = QActionGroup(self) ag_views.setExclusive(True) for v in self.views.keys(): a = QAction(v) a.triggered.connect(self.change_view) a.setCheckable(True) ag_views.addAction(a) self.tb_views.addActions(ag_views.actions()) ag_views.actions()[0].setChecked(True) stretch = QWidget() stretch.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)) self.tb_views.addWidget(stretch) # actEditView = self.tb_views.addAction("Edit views...") # def create_view_filter(self): # # self.tb_filter.addWidget(QLabel("Show devices: ")) # # self.cbxLWT = QComboBox() # # self.cbxLWT.addItems(["All", "Online"d, "Offline"]) # # self.cbxLWT.currentTextChanged.connect(self.build_filter_regex) # # self.tb_filter.addWidget(self.cbxLWT) # # self.tb_filter.addWidget(QLabel(" Search: ")) # self.leSearch = QLineEdit() # self.leSearch.setClearButtonEnabled(True) # self.leSearch.textChanged.connect(self.build_filter_regex) # self.tb_filter.addWidget(self.leSearch) # # def build_filter_regex(self, txt): # query = self.leSearch.text() # # if self.cbxLWT.currentText() != "All": # # query = "{}|{}".format(self.cbxLWT.currentText(), query) # self.sorted_device_model.setFilterRegExp(query) def change_view(self, a=None): view = self.views[self.sender().text()] self.model.setupColumns(view) self.device_list.setupView(view) def ctx_menu_copy(self): if self.idx: string = dumps(self.model.data(self.idx)) if string.startswith('"') and string.endswith('"'): string = string[1:-1] QApplication.clipboard().setText(string) def ctx_menu_clear_retained(self): if self.device: relays = self.device.power() if relays and len(relays.keys()) > 0: for r in relays.keys(): self.mqtt.publish(self.device.cmnd_topic(r), retain=True) QMessageBox.information(self, "Clear retained", "Cleared retained messages.") def ctx_menu_clear_backlog(self): if self.device: self.mqtt.publish(self.device.cmnd_topic("backlog"), "") QMessageBox.information(self, "Clear Backlog", "Backlog cleared.") def ctx_menu_restart(self): if self.device: self.mqtt.publish(self.device.cmnd_topic("restart"), payload="1") for k in list(self.device.power().keys()): self.device.p.pop(k) def ctx_menu_reset(self): if self.device: reset, ok = QInputDialog.getItem(self, "Reset device and restart", "Select reset mode", resets, editable=False) if ok: self.mqtt.publish(self.device.cmnd_topic("reset"), payload=reset.split(":")[0]) for k in list(self.device.power().keys()): self.device.p.pop(k) def ctx_menu_refresh(self): if self.device: for k in list(self.device.power().keys()): self.device.p.pop(k) for c in initial_commands(): cmd, payload = c cmd = self.device.cmnd_topic(cmd) self.mqtt.publish(cmd, payload, 1) def ctx_menu_delete_device(self): if self.device: if QMessageBox.question(self, "Confirm", "Do you want to remove the following device?\n'{}' ({})" .format(self.device.p['FriendlyName1'], self.device.p['Topic'])) == QMessageBox.Yes: self.model.deleteDevice(self.idx) def ctx_menu_teleperiod(self): if self.device: teleperiod, ok = QInputDialog.getInt(self, "Set telemetry period", "Input 1 to reset to default\n[Min: 10, Max: 3600]", self.device.p['TelePeriod'], 1, 3600) if ok: if teleperiod != 1 and teleperiod < 10: teleperiod = 10 self.mqtt.publish(self.device.cmnd_topic("teleperiod"), teleperiod) def ctx_menu_config_backup(self): if self.device: self.backup = bytes() self.dl = self.nam.get(QNetworkRequest(QUrl("http://{}/dl".format(self.device.p['IPAddress'])))) self.dl.readyRead.connect(self.get_dump) self.dl.finished.connect(self.save_dump) def ctx_menu_ota_set_url(self): if self.device: url, ok = QInputDialog.getText(self, "Set OTA URL", '100 chars max. Set to "1" to reset to default.', text=self.device.p['OtaUrl']) if ok: self.mqtt.publish(self.device.cmnd_topic("otaurl"), payload=url) def ctx_menu_ota_set_upgrade(self): if self.device: if QMessageBox.question(self, "OTA Upgrade", "Are you sure to OTA upgrade from\n{}".format(self.device.p['OtaUrl']), QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes: self.mqtt.publish(self.device.cmnd_topic("upgrade"), payload="1") def show_list_ctx_menu(self, at): self.select_device(self.device_list.indexAt(at)) self.ctx_menu.popup(self.device_list.viewport().mapToGlobal(at)) def select_device(self, idx): self.idx = self.sorted_device_model.mapToSource(idx) self.device = self.model.deviceAtRow(self.idx.row()) self.deviceSelected.emit(self.device) relays = self.device.power() self.agAllPower.setEnabled(len(relays) >= 1) for i, a in enumerate(self.agRelays.actions()): a.setVisible(len(relays) > 1 and i < len(relays)) color = self.device.color().get("Color", False) has_color = bool(color) self.actColor.setEnabled(has_color and not self.device.setoption(68)) self.actChannels.setEnabled(has_color) if has_color: self.actChannels.menu().clear() max_val = 100 if self.device.setoption(15) == 0: max_val = 1023 for k, v in self.device.pwm().items(): channel = SliderAction(self, k) channel.slider.setMaximum(max_val) channel.slider.setValue(int(v)) self.mChannels.addAction(channel) channel.slider.valueChanged.connect(self.set_channel) dimmer = self.device.color().get("Dimmer") if dimmer: saDimmer = SliderAction(self, "Dimmer") saDimmer.slider.setValue(int(dimmer)) self.mChannels.addAction(saDimmer) saDimmer.slider.valueChanged.connect(self.set_channel) def toggle_power(self, action): if self.device: idx = self.agRelays.actions().index(action) relay = list(self.device.power().keys())[idx] self.mqtt.publish(self.device.cmnd_topic(relay), "toggle") def toggle_power_all(self, action): if self.device: idx = self.agAllPower.actions().index(action) for r in self.device.power().keys(): self.mqtt.publish(self.device.cmnd_topic(r), str(not bool(idx))) def set_color(self): if self.device: color = self.device.color().get("Color") if color: dlg = QColorDialog() new_color = dlg.getColor(QColor("#{}".format(color))) if new_color.isValid(): new_color = new_color.name() if new_color != color: self.mqtt.publish(self.device.cmnd_topic("color"), new_color) def set_channel(self, value=0): cmd = self.sender().objectName() if self.device: self.mqtt.publish(self.device.cmnd_topic(cmd), str(value)) def configureSO(self): if self.device: dlg = SetOptionsDialog(self.device) dlg.sendCommand.connect(self.mqtt.publish) dlg.exec_() def configureModule(self): if self.device: dlg = ModuleDialog(self.device) dlg.sendCommand.connect(self.mqtt.publish) dlg.exec_() def configureGPIO(self): if self.device: dlg = GPIODialog(self.device) dlg.sendCommand.connect(self.mqtt.publish) dlg.exec_() def configureTemplate(self): if self.device: dlg = TemplateDialog(self.device) dlg.sendCommand.connect(self.mqtt.publish) dlg.exec_() def configureTimers(self): if self.device: self.mqtt.publish(self.device.cmnd_topic("timers")) timers = TimersDialog(self.device) self.mqtt.messageSignal.connect(timers.parseMessage) timers.sendCommand.connect(self.mqtt.publish) timers.exec_() def configureButtons(self): if self.device: backlog = [] buttons = ButtonsDialog(self.device) if buttons.exec_() == QDialog.Accepted: for c, cw in buttons.command_widgets.items(): current_value = self.device.p.get(c) new_value = "" if isinstance(cw.input, SpinBox): new_value = cw.input.value() if isinstance(cw.input, QComboBox): new_value = cw.input.currentIndex() if current_value != new_value: backlog.append("{} {}".format(c, new_value)) for so, sow in buttons.setoption_widgets.items(): current_value = self.device.setoption(so) new_value = -1 if isinstance(sow.input, SpinBox): new_value = sow.input.value() if isinstance(sow.input, QComboBox): new_value = sow.input.currentIndex() if current_value != new_value: backlog.append("SetOption{} {}".format(so, new_value)) if backlog: backlog.append("status 3") self.mqtt.publish(self.device.cmnd_topic("backlog"), "; ".join(backlog)) def configureSwitches(self): if self.device: backlog = [] switches = SwitchesDialog(self.device) if switches.exec_() == QDialog.Accepted: for c, cw in switches.command_widgets.items(): current_value = self.device.p.get(c) new_value = "" if isinstance(cw.input, SpinBox): new_value = cw.input.value() if isinstance(cw.input, QComboBox): new_value = cw.input.currentIndex() if current_value != new_value: backlog.append("{} {}".format(c, new_value)) for so, sow in switches.setoption_widgets.items(): current_value = self.device.setoption(so) new_value = -1 if isinstance(sow.input, SpinBox): new_value = sow.input.value() if isinstance(sow.input, QComboBox): new_value = sow.input.currentIndex() if current_value != new_value: backlog.append("SetOption{} {}".format(so, new_value)) for sw, sw_mode in enumerate(self.device.p['SwitchMode']): new_value = switches.sm.inputs[sw].currentIndex() if sw_mode != new_value: backlog.append("switchmode{} {}".format(sw+1, new_value)) if backlog: backlog.append("status") backlog.append("status 3") self.mqtt.publish(self.device.cmnd_topic("backlog"), "; ".join(backlog)) def configurePower(self): if self.device: backlog = [] power = PowerDialog(self.device) if power.exec_() == QDialog.Accepted: for c, cw in power.command_widgets.items(): current_value = self.device.p.get(c) new_value = "" if isinstance(cw.input, SpinBox): new_value = cw.input.value() if isinstance(cw.input, QComboBox): new_value = cw.input.currentIndex() if current_value != new_value: backlog.append("{} {}".format(c, new_value)) for so, sow in power.setoption_widgets.items(): new_value = -1 if isinstance(sow.input, SpinBox): new_value = sow.input.value() if isinstance(sow.input, QComboBox): new_value = sow.input.currentIndex() if new_value != self.device.setoption(so): backlog.append("SetOption{} {}".format(so, new_value)) new_interlock_value = power.ci.input.currentData() new_interlock_grps = " ".join([grp.text().replace(" ", "") for grp in power.ci.groups]).rstrip() if new_interlock_value != self.device.p.get("Interlock", "OFF"): backlog.append("interlock {}".format(new_interlock_value)) if new_interlock_grps != self.device.p.get("Groups", ""): backlog.append("interlock {}".format(new_interlock_grps)) for i, pt in enumerate(power.cpt.inputs): ptime = "PulseTime{}".format(i+1) current_ptime = self.device.p.get(ptime) if current_ptime: current_value = list(current_ptime.keys())[0] new_value = str(pt.value()) if new_value != current_value: backlog.append("{} {}".format(ptime, new_value)) if backlog: backlog.append("status") backlog.append("status 3") self.mqtt.publish(self.device.cmnd_topic("backlog"), "; ".join(backlog)) def get_dump(self): self.backup += self.dl.readAll() def save_dump(self): fname = self.dl.header(QNetworkRequest.ContentDispositionHeader) if fname: fname = fname.split('=')[1] save_file = QFileDialog.getSaveFileName(self, "Save config backup", "{}/TDM/{}".format(QDir.homePath(), fname))[0] if save_file: with open(save_file, "wb") as f: f.write(self.backup) def check_fulltopic(self, fulltopic): fulltopic += "/" if not fulltopic.endswith('/') else '' return "%prefix%" in fulltopic and "%topic%" in fulltopic def closeEvent(self, event): event.ignore()
class MainGui(QMainWindow): def __init__(self): self.sniffer=Sniffer() self.sniffer.interface,ok=QInputDialog.getItem(QWidget(),'Sniffer','Welcome!\n\nChoose Interface:',\ self.sniffer.interfaces,0,False) if ok: super().__init__() self.initUI() self.reassembler=Reassembler() else: exit() def initUI(self): '''Define action''' self.open_act=QAction(QIcon('./icons/open.png'),'Open',self) self.open_act.setShortcut('Ctrl+O') self.open_act.triggered.connect(self.OpenFile) self.save_act=QAction(QIcon('./icons/save.png'),'Save',self) self.save_act.setShortcut('Ctrl+S') self.save_act.triggered.connect(self.SaveFile) self.save_act.setEnabled(False) self.quit_act=QAction(QIcon('./icons/quit.png'),'Quit',self) self.quit_act.setShortcut('Ctrl+Q') self.quit_act.triggered.connect(self.close) self.filter_find_act=QAction(QIcon('./icons/filter&find.png'),'Filter and Find',self) self.filter_find_act.setShortcut('Ctrl+F') self.filter_find_act.triggered.connect(self.FilterPackets) self.filter_find_act.setEnabled(False) self.reassemble_act=QAction(QIcon('./icons/reassemble.png'),'Reassemble',self) self.reassemble_act.setShortcut('Ctrl+Alt+R') self.reassemble_act.triggered.connect(self.BrowseReassembly) self.reassemble_act.setEnabled(False) self.start_act=QAction(QIcon('./icons/start.png'),'Start',self) self.start_act.setShortcut('Ctrl+E') self.start_act.triggered.connect(self.StartCapture) self.stop_act=QAction(QIcon('./icons/stop.png'),'Stop',self) self.stop_act.setShortcut('Ctrl+E') self.stop_act.triggered.connect(self.StopCapture) self.stop_act.setEnabled(False) self.iface_act_group=QActionGroup(self) self.iface_act_group.triggered.connect(self.UpdateInterface) self.status_label=QLabel() self.statusBar().addPermanentWidget(self.status_label) self.statusBar().showMessage('Ready to capture') '''Define menu''' menubar=self.menuBar() file_menu=menubar.addMenu('&File') analyze_menu=menubar.addMenu('&Analyze') interface_menu=menubar.addMenu('&Interface') file_menu.addAction(self.open_act) file_menu.addAction(self.save_act) file_menu.addAction(self.quit_act) analyze_menu.addAction(self.filter_find_act) analyze_menu.addAction(self.reassemble_act) '''Interface choose menu''' for iface in self.sniffer.interfaces: iface_act=QAction(iface,self,checkable=True) if iface==self.sniffer.interface: iface_act.setChecked(True) self.iface_act_group.addAction(iface_act) interface_menu.addAction(iface_act) toolbar=self.addToolBar('Toolbar') toolbar.setMovable(False) toolbar.addAction(self.start_act) toolbar.addAction(self.stop_act) toolbar.addSeparator() toolbar.addAction(self.open_act) toolbar.addAction(self.save_act) toolbar.addSeparator() toolbar.addAction(self.filter_find_act) toolbar.addAction(self.reassemble_act) '''Main region for filter and find function filter fields include source address and port, destination address and port, protocol, and a search field ''' filter_label=QLabel('Filter:') source_label=QLabel('Source') destination_label=QLabel('Destination') protocol_label=QLabel('Protocol') find_label=QLabel(' Find:') self.source_edit=QLineEdit() self.sport_edit=QLineEdit() self.destination_edit=QLineEdit() self.dport_edit=QLineEdit() self.protocol_edit=QLineEdit() self.find_edit=QLineEdit() self.source_edit.setClearButtonEnabled(True) self.sport_edit.setClearButtonEnabled(True) self.destination_edit.setClearButtonEnabled(True) self.dport_edit.setClearButtonEnabled(True) self.protocol_edit.setClearButtonEnabled(True) self.find_edit.setClearButtonEnabled(True) self.protocol_completer=QCompleter() self.protocol_completer.setCaseSensitivity(False) self.protocol_edit.setCompleter(self.protocol_completer) self.source_edit.textChanged.connect(self.UpdateSourceFilter) self.sport_edit.textChanged.connect(self.UpdateSportFilter) self.destination_edit.textChanged.connect(self.UpdateDestinationFilter) self.dport_edit.textChanged.connect(self.UpdateDportFilter) self.protocol_edit.textChanged.connect(self.UpdateProtocolFilter) self.find_edit.textChanged.connect(self.UpdateFindFilter) '''Main region to display captured or filtered packets''' self.packet_table=QTableWidget() self.packet_table.setEditTriggers(QTableWidget.NoEditTriggers) self.packet_table.setSelectionBehavior(QTableWidget.SelectRows) self.packet_table.setSelectionMode(QTableWidget.SingleSelection) self.packet_table.setShowGrid(False) self.packet_table.setMinimumHeight(80) self.packet_table.itemSelectionChanged.connect(self.BrowseSelectedPacket) self.packet_table.verticalHeader().hide() self.packet_table.verticalHeader().setDefaultSectionSize(25) self.packet_table.setColumnCount(7) self.packet_table.setHorizontalHeaderLabels(['No.','Time','Source','Destination','Protocol','Length','Info']) self.packet_table.setColumnWidth(0,60) self.packet_table.setColumnWidth(1,120) self.packet_table.setColumnWidth(2,160) self.packet_table.setColumnWidth(3,160) self.packet_table.setColumnWidth(4,100) self.packet_table.setColumnWidth(5,60) self.packet_table.setColumnWidth(6,320) self.packet_table.horizontalHeader().setStretchLastSection(True) '''Main region to browse detailed information about packet''' self.details_browser=QTextBrowser() self.bytes_browser=QTextBrowser() self.details_browser.setMinimumHeight(80) self.bytes_browser.setMinimumHeight(80) horizontal_splitter=QSplitter(Qt.Horizontal) vertical_splitter=QSplitter(Qt.Vertical) horizontal_splitter.addWidget(self.details_browser) horizontal_splitter.addWidget(self.bytes_browser) vertical_splitter.addWidget(self.packet_table) vertical_splitter.addWidget(horizontal_splitter) vertical_splitter.setSizes([1,1]) '''Design mainwindow layout''' grid=QGridLayout() grid.addWidget(filter_label,0,0,1,1) grid.addWidget(source_label,0,1,1,1) grid.addWidget(self.source_edit,0,2,1,1) grid.addWidget(self.sport_edit,0,3,1,1) grid.addWidget(destination_label,0,4,1,1) grid.addWidget(self.destination_edit,0,5,1,1) grid.addWidget(self.dport_edit,0,6,1,1) grid.addWidget(protocol_label,0,7,1,1) grid.addWidget(self.protocol_edit,0,8,1,1) grid.addWidget(find_label,0,9,1,1) grid.addWidget(self.find_edit,0,10,1,1) grid.addWidget(vertical_splitter,1,0,1,11) grid.setColumnStretch(2,3) grid.setColumnStretch(3,1) grid.setColumnStretch(5,3) grid.setColumnStretch(6,1) grid.setColumnStretch(8,3) grid.setColumnStretch(10,2) central=QWidget() self.setCentralWidget(central) central.setLayout(grid) self.setGeometry(100,60,1120,630) self.setWindowIcon(QIcon('./icons/sniffer.png')) self.setWindowTitle('Sniffer') self.show() def closeEvent(self,event): '''Override default closeEvent of MainWindow This is for checking before quit, also finish up jobs before quit ''' reply=QMessageBox.question(self,'Quit','Are you sure?', QMessageBox.Yes|QMessageBox.No,QMessageBox.Yes) if reply==QMessageBox.Yes: self.sniffer.capturing=False self.reassembler.close() event.accept() else: event.ignore() def UpdateInterface(self,iface_act): '''When selection in interface_menu changes, update interface''' self.sniffer.interface=iface_act.text() def StartCapture(self): '''Start capturing packets''' if not self.sniffer.capturing: self.sniffer.Sniff() self.packet_table.setRowCount(0) self.statusBar().showMessage(self.sniffer.interface+': live capture in progress') self.status_label.clear() self.open_act.setEnabled(False) self.save_act.setEnabled(False) self.filter_find_act.setEnabled(False) self.start_act.setEnabled(False) self.stop_act.setEnabled(True) self.iface_act_group.setEnabled(False) def StopCapture(self): '''Stop capturing packets''' self.sniffer.StopSniffing() self.statusBar().showMessage('Ready to capture') if self.sniffer.total_number: self.status_label.setText('Packets: {} · Displayed: {} ({:.1%})'.format(self.sniffer.total_number,\ len(self.sniffer.packet_filter.packet_list),len(self.sniffer.packet_filter.packet_list)/self.sniffer.total_number)) self.open_act.setEnabled(True) self.save_act.setEnabled(True) self.filter_find_act.setEnabled(True) self.start_act.setEnabled(True) self.stop_act.setEnabled(False) self.iface_act_group.setEnabled(True) def OpenFile(self): '''Open a pcap file''' file=QFileDialog.getOpenFileName(self,'Open Packets','./','pcap(*.pcap)')[0] if file: self.packet_table.setRowCount(0) self.statusBar().showMessage('Loading: '+file) self.sniffer.OpenPackets(file) self.statusBar().showMessage('Ready to capture') if self.sniffer.total_number: self.status_label.setText('Packets: {} · Displayed: {} ({:.1%})'.format(self.sniffer.total_number,\ len(self.sniffer.packet_filter.packet_list),len(self.sniffer.packet_filter.packet_list)/self.sniffer.total_number)) self.save_act.setEnabled(True) self.filter_find_act.setEnabled(True) def SaveFile(self): '''Save packets as unreadable pcap or readable txt''' file=QFileDialog.getSaveFileName(self,'Save Packets','./','unreadable(*.pcap);;readable(*.txt)')[0] if file: self.sniffer.SavePackets(file) def FilterPackets(self): '''Filter packets and display filtered packets''' self.sniffer.packet_filter.FilterPackets() self.packet_table.setRowCount(0) for row_number,packet_tuple in enumerate(self.sniffer.packet_filter.packet_list): self.packet_table.insertRow(row_number) for column in range(7): self.packet_table.setItem(row_number,column,QTableWidgetItem(packet_tuple[column])) self.packet_table.item(row_number,column).setBackground(packet_tuple[7]) self.packet_table.item(row_number,column).setForeground(packet_tuple[8]) self.packet_table.scrollToBottom() if self.sniffer.total_number: self.status_label.setText('Packets: {} · Displayed: {} ({:.1%})'.format(self.sniffer.total_number,\ len(self.sniffer.packet_filter.packet_list),len(self.sniffer.packet_filter.packet_list)/self.sniffer.total_number)) '''Following functions update filter string when text in the filter and find region changes Also, inputs are checked if valid, and background change as a prompt ''' def UpdateSourceFilter(self): self.sniffer.packet_filter.src_filter=self.source_edit.text() self.sniffer.packet_filter.src_filter_enable=\ bool(match(self.sniffer.packet_filter.address_re,self.sniffer.packet_filter.src_filter)) if self.sniffer.packet_filter.src_filter=='': self.source_edit.setStyleSheet('QLineEdit { background-color: rgb(255, 255, 255) }') elif self.sniffer.packet_filter.src_filter_enable: self.source_edit.setStyleSheet('QLineEdit { background-color: rgb(175, 255, 175) }') else: self.source_edit.setStyleSheet('QLineEdit { background-color: rgb(255, 175, 175) }') def UpdateSportFilter(self): self.sniffer.packet_filter.sport_filter=self.sport_edit.text() self.sniffer.packet_filter.sport_filter_enable=\ bool(match(self.sniffer.packet_filter.port_re,self.sniffer.packet_filter.sport_filter)) if self.sniffer.packet_filter.sport_filter=='': self.sport_edit.setStyleSheet('QLineEdit { background-color: rgb(255, 255, 255) }') elif self.sniffer.packet_filter.sport_filter_enable: self.sport_edit.setStyleSheet('QLineEdit { background-color: rgb(175, 255, 175) }') else: self.sport_edit.setStyleSheet('QLineEdit { background-color: rgb(255, 175, 175) }') def UpdateDestinationFilter(self): self.sniffer.packet_filter.dst_filter=self.destination_edit.text() self.sniffer.packet_filter.dst_filter_enable=\ bool(match(self.sniffer.packet_filter.address_re,self.sniffer.packet_filter.dst_filter)) if self.sniffer.packet_filter.dst_filter=='': self.destination_edit.setStyleSheet('QLineEdit { background-color: rgb(255, 255, 255) }') elif self.sniffer.packet_filter.dst_filter_enable: self.destination_edit.setStyleSheet('QLineEdit { background-color: rgb(175, 255, 175) }') else: self.destination_edit.setStyleSheet('QLineEdit { background-color: rgb(255, 175, 175) }') def UpdateDportFilter(self): self.sniffer.packet_filter.dport_filter=self.dport_edit.text() self.sniffer.packet_filter.dport_filter_enable=\ bool(match(self.sniffer.packet_filter.port_re,self.sniffer.packet_filter.dport_filter)) if self.sniffer.packet_filter.dport_filter=='': self.dport_edit.setStyleSheet('QLineEdit { background-color: rgb(255, 255, 255) }') elif self.sniffer.packet_filter.dport_filter_enable: self.dport_edit.setStyleSheet('QLineEdit { background-color: rgb(175, 255, 175) }') else: self.dport_edit.setStyleSheet('QLineEdit { background-color: rgb(255, 175, 175) }') def UpdateProtocolFilter(self): '''The completer makes protocol filter input more convinient''' if self.sniffer.capturing: protocol_set=set([packet_tuple[4] for packet_tuple in self.sniffer.packet_filter.packet_list]) self.protocol_completer.setModel(QStringListModel(protocol_set,self.protocol_completer)) elif not self.sniffer.packet_filter.protocol_set: protocol_set=set([packet_tuple[4] for packet_tuple in self.sniffer.total_packets]) self.protocol_completer.setModel(QStringListModel(protocol_set,self.protocol_completer)) self.sniffer.packet_filter.protocol_set=protocol_set else: protocol_set=self.sniffer.packet_filter.protocol_set self.sniffer.packet_filter.protocol_filter=self.protocol_edit.text() self.sniffer.packet_filter.protocol_filter_enable=self.sniffer.packet_filter.protocol_filter in protocol_set if self.sniffer.packet_filter.protocol_filter=='': self.protocol_edit.setStyleSheet('QLineEdit { background-color: rgb(255, 255, 255) }') elif self.sniffer.packet_filter.protocol_filter_enable: self.protocol_edit.setStyleSheet('QLineEdit { background-color: rgb(175, 255, 175) }') else: self.protocol_edit.setStyleSheet('QLineEdit { background-color: rgb(255, 175, 175) }') def UpdateFindFilter(self): self.sniffer.packet_filter.find_filter=self.find_edit.text() def BrowseReassembly(self): '''This is for browse TCP or IP reassembly''' selected=self.packet_table.selectedItems() packet_tuple=self.sniffer.packet_filter.packet_list[selected[0].row()] if self.reassembler.is_tcp_segment: self.reassembler.ReassembleTCP(packet_tuple) self.reassembler.setWindowTitle('TCP Reassembly') reassembly=str(len(self.reassembler.packet_numbers))+' Reassembled TCP Segments:\n#' else: self.reassembler.ReassembleIP(packet_tuple) self.reassembler.setWindowTitle('IP Reassembly') reassembly=str(len(self.reassembler.packet_numbers))+' Reassembled IP Fragments:\n#' reassembly+=', #'.join(self.reassembler.packet_numbers)+'\n\n' reassembly+=self.sniffer.hexdump(self.reassembler.reassembly) self.reassembler.browser.setText(reassembly) self.reassembler.show() def BrowseSelectedPacket(self): '''Called Upon selection in packet table changes Browse detailed information and hexdump about packet ''' selected=self.packet_table.selectedItems() if selected: packet=self.sniffer.packet_filter.packet_list[selected[0].row()] self.details_browser.setText(packet[-1].show(dump=True)) self.bytes_browser.setText(self.sniffer.hexdump(packet[-1])) self.reassemble_act.setEnabled(self.reassembler.isFragment(packet)) else: self.details_browser.clear() self.bytes_browser.clear() self.reassemble_act.setEnabled(False) def UpdatePacketTable(self,packet): '''Called each time sniffer captures a packet Thus glitches do appear when too many packets captured in a short time The function collects necessary info about packets and display in the table ''' row_number=self.packet_table.rowCount() if row_number: time=packet.time-self.sniffer.initial_time else: self.sniffer.initial_time=packet.time self.sniffer.sniffed_time=0 time=0 layer_number=2 layer=packet.getlayer(layer_number) while layer and layer.name not in ('Raw','Padding'): layer_number+=1 layer=packet.getlayer(layer_number) protocol=packet.getlayer(layer_number-1).name if protocol=='ARP': source=packet.getlayer(1).hwsrc destination=packet.getlayer(1).hwdst else: source=packet.getlayer(1).src destination=packet.getlayer(1).dst '''Further analyse protocol through ports''' if packet.haslayer(TCP) or packet.haslayer(UDP): sport,dport=packet.sport,packet.dport if protocol=='TCP': sport_string=packet[TCP].fields_desc[0].i2repr(packet,sport) dport_string=packet[TCP].fields_desc[1].i2repr(packet,dport) if sport_string!=str(sport): protocol=sport_string.upper() if dport_string!=str(dport): protocol=dport_string.upper() elif protocol=='UDP': sport_string=packet[UDP].fields_desc[0].i2repr(packet,sport) dport_string=packet[UDP].fields_desc[1].i2repr(packet,dport) if sport_string!=str(sport): protocol=sport_string.upper() if dport_string!=str(dport): protocol=dport_string.upper() else: sport,dport=None,None info_list=packet.summary().split(' / ')[1:] info_list.sort(key=lambda x:len(x)) '''The coloring rules are the default ones in Wireshark''' if protocol=='ARP': background,foreground=QColor(250,240,215),QColor(18,39,46) #ARP elif 'ICMP' in protocol: if protocol=='ICMP' and packet[ICMP].type in (3,4,5,11): background,foreground=QColor(18,39,46),QColor(183,247,116) #ICMP errors else: background,foreground=QColor(252,224,255),QColor(18,39,46) #ICMP or ICMPv6 elif packet.haslayer(TCP): flag=int(packet[TCP].flags) if flag>>2&1: background,foreground=QColor(164,0,0),QColor(255,252,156) #TCP reset elif sport==80 or dport==80: background,foreground=QColor(228,255,199),QColor(18,39,46) #HTTP elif flag&3: background,foreground=QColor(160,160,160),QColor(18,39,46) #TCP SYN/FIN else: background,foreground=QColor(231,230,255),QColor(18,39,46) #TCP elif packet.haslayer(UDP): background,foreground=QColor(218,238,255),QColor(18,39,46) #UDP else: background,foreground=QColor(255,255,255),QColor(18,39,46) '''('No.','Time','Source','Destination','Protocol','Length','Info',background,foreground,sport,dport,packet)''' packet_tuple=(str(row_number),'{:.6f}'.format(time),source,destination,\ protocol,str(len(packet)),info_list[-1],background,foreground,\ sport,dport,packet) self.sniffer.packet_filter.packet_list.append(packet_tuple) self.packet_table.insertRow(row_number) for column in range(7): self.packet_table.setItem(row_number,column,QTableWidgetItem(packet_tuple[column])) self.packet_table.item(row_number,column).setBackground(background) self.packet_table.item(row_number,column).setForeground(foreground) '''Avoid scrolling too fast''' if time>0.05+self.sniffer.sniffed_time: self.packet_table.scrollToBottom() self.sniffer.sniffed_time=time