class DeviceList(GenericList): __gsignals__: GSignals = { # @param: device TreeIter # note: None None is given when there ar no more rows, or when selected device is removed 'device-selected': (GObject.SignalFlags.RUN_LAST, None, (Device, Gtk.TreeIter,)), # @param: device, TreeIter, (key, value) 'device-property-changed': (GObject.SignalFlags.RUN_LAST, None, (Device, Gtk.TreeIter, object,)), # @param: adapter, (key, value) 'adapter-property-changed': (GObject.SignalFlags.RUN_LAST, None, (Adapter, object,)), # @param: progress (0 to 1) 'discovery-progress': (GObject.SignalFlags.RUN_LAST, None, (float,)), # @param: new adapter path, None if there are no more adapters 'adapter-changed': (GObject.SignalFlags.RUN_LAST, None, (str,)), # @param: adapter path 'adapter-added': (GObject.SignalFlags.RUN_LAST, None, (str,)), 'adapter-removed': (GObject.SignalFlags.RUN_LAST, None, (str,)), } def __del__(self): logging.debug("deleting mainlist") super().__del__() def __init__(self, adapter_name=None, tabledata=None, **kwargs): if not tabledata: tabledata = [] # cache for fast lookup in the list self.path_to_row: Dict[str, Gtk.TreeRowReference] = {} self.monitored_devices: List[str] = [] self.manager = Manager() self._managerhandlers: List[int] = [] self._managerhandlers.append(self.manager.connect_signal('adapter-removed', self.__on_manager_signal, 'adapter-removed')) self._managerhandlers.append(self.manager.connect_signal('adapter-added', self.__on_manager_signal, 'adapter-added')) self._managerhandlers.append(self.manager.connect_signal('device-created', self.__on_manager_signal, 'device-created')) self._managerhandlers.append(self.manager.connect_signal('device-removed', self.__on_manager_signal, 'device-removed')) self.any_device = AnyDevice() self._anydevhandler = self.any_device.connect_signal("property-changed", self._on_device_property_changed) self.__discovery_time = 0 self.__adapter_path = None self.Adapter = None self.discovering = False data = tabledata + [ {"id": "device", "type": object}, {"id": "dbus_path", "type": str}, {"id": "timestamp", "type": float} ] super().__init__(data, **kwargs) self.set_name("DeviceList") self.set_adapter(adapter_name) self._any_adapter = AnyAdapter() self._anyadapterhandler = self._any_adapter.connect_signal("property-changed", self._on_property_changed) self._selectionhandler = self.selection.connect('changed', self.on_selection_changed) self.icon_theme = Gtk.IconTheme.get_default() self.icon_theme.prepend_search_path(ICON_PATH) # handle icon theme changes self.icon_theme.connect("changed", self.on_icon_theme_changed) def destroy(self): self.any_device.disconnect(self._anydevhandler) self._any_adapter.disconnect(self._anyadapterhandler) self.selection.disconnect(self._selectionhandler) for handler in self._managerhandlers: self.manager.disconnect(handler) super().destroy() def __on_manager_signal(self, manager, path, signal_name): if signal_name == 'adapter-removed': self.emit("adapter-removed", path) if path == self.__adapter_path: self.clear() self.Adapter = None self.set_adapter() if signal_name == 'adapter-added': if self.Adapter is None: self.set_adapter(path) self.emit("adapter-added", path) if signal_name == 'device-created': tree_iter = self.find_device_by_path(path) if tree_iter is None: dev = Device(obj_path=path) self.device_add_event(dev) if signal_name == 'device-removed': tree_iter = self.find_device_by_path(path) if tree_iter: row = self.get(tree_iter, "device") dev = row["device"] self.device_remove_event(dev) def on_selection_changed(self, selection): _model, tree_iter = selection.get_selected() if tree_iter: row = self.get(tree_iter, "device") dev = row["device"] self.emit("device-selected", dev, tree_iter) def _on_property_changed(self, _adapter, key, value, path): if not self.Adapter or self.Adapter.get_object_path() != path: return if key == "Discovering": if not value and self.discovering: self.stop_discovery() self.emit("adapter-property-changed", self.Adapter, (key, value)) def _on_device_property_changed(self, _device, key, value, path): tree_iter = self.find_device_by_path(path) if tree_iter is not None: dev = self.get(tree_iter, "device")["device"] self.row_update_event(tree_iter, key, value) self.emit("device-property-changed", dev, tree_iter, (key, value)) if key == "Connected": if value: self.monitor_power_levels(dev) else: r = Gtk.TreeRowReference.new(self.get_model(), self.props.model.get_path(tree_iter)) self.level_setup_event(r, dev, None) # Override when subclassing def on_icon_theme_changed(self, widget): logging.warning("Icons may not be updated with icon theme changes") def monitor_power_levels(self, device): def update(row_ref, cinfo, address): if not row_ref.valid(): logging.warning("stopping monitor (row does not exist)") cinfo.deinit() self.monitored_devices.remove(address) return False if not self.get_model(): self.monitored_devices.remove(address) return False if not device['Connected']: logging.info("stopping monitor (not connected)") cinfo.deinit() self.level_setup_event(row_ref, device, None) self.monitored_devices.remove(address) return False else: self.level_setup_event(row_ref, device, cinfo) return True bt_address = device["Address"] if device["Connected"] and bt_address not in self.monitored_devices: logging.info("starting monitor") tree_iter = self.find_device(device) assert self.Adapter is not None hci = os.path.basename(self.Adapter.get_object_path()) cinfo = conn_info(bt_address, hci) try: cinfo.init() except ConnInfoReadError: logging.warning("Failed to get power levels, probably a LE device.") r = Gtk.TreeRowReference.new(self.get_model(), self.get_model().get_path(tree_iter)) self.level_setup_event(r, device, cinfo) GLib.timeout_add(1000, update, r, cinfo, bt_address) self.monitored_devices.append(bt_address) # ##### virtual funcs ##### # called when power levels need updating # if cinfo is None then info icons need to be removed def level_setup_event(self, tree_iter, device, cinfo): pass # called when row needs to be initialized def row_setup_event(self, tree_iter, device): pass # called when a property for a device changes def row_update_event(self, tree_iter, key, value): pass # called when device needs to be added to the list def device_add_event(self, device): self.add_device(device) def device_remove_event(self, device): logging.debug(device) tree_iter = self.find_device(device) if self.compare(self.selected(), tree_iter): self.emit("device-selected", None, None) self.delete(tree_iter) ######################### def set_adapter(self, adapter=None): self.clear() if self.discovering: self.stop_discovery() self.emit("adapter-property-changed", self.Adapter, ("Discovering", False)) adapter = adapter_path_to_name(adapter) logging.debug(f"Setting adapter to: {adapter}") if adapter is not None: try: self.Adapter = self.manager.get_adapter(adapter) self.__adapter_path = self.Adapter.get_object_path() except DBusNoSuchAdapterError: logging.warning('Failed to set adapter, trying first available.') self.set_adapter(None) return else: adapters = self.manager.get_adapters() if len(adapters) > 0: self.Adapter = adapters[0] self.__adapter_path = self.Adapter.get_object_path() else: self.Adapter = None self.__adapter_path = None self.emit("adapter-changed", self.__adapter_path) def update_progress(self, time, totaltime): if not self.discovering: return False self.__discovery_time += time progress = self.__discovery_time / totaltime if progress >= 1.0: progress = 1.0 if self.__discovery_time >= totaltime: self.stop_discovery() return False self.emit("discovery-progress", progress) return True def add_device(self, device): # device belongs to another adapter if not self.Adapter or not device['Adapter'] == self.Adapter.get_object_path(): return logging.info("adding new device") tree_iter = self.liststore.append() self.set(tree_iter, device=device) self.row_setup_event(tree_iter, device) object_path = device.get_object_path() timestamp = datetime.strftime(datetime.now(), '%Y%m%d%H%M%S%f') self.set(tree_iter, dbus_path=object_path, timestamp=float(timestamp)) if device["Connected"]: self.monitor_power_levels(device) def display_known_devices(self, autoselect=False): self.clear() if self.Adapter: devices = self.manager.get_devices(self.Adapter.get_object_path()) for device in devices: self.device_add_event(device) if autoselect: self.selection.select_path(0) def discover_devices(self, time=10.24): if not self.discovering: self.__discovery_time = 0 if self.Adapter is not None: self.Adapter.start_discovery() self.discovering = True t = 1.0 / 15 * 1000 GLib.timeout_add(int(t), self.update_progress, t / 1000, time) def is_valid_adapter(self): if self.Adapter is None: return False else: return True def get_adapter_path(self): if self.is_valid_adapter(): return self.__adapter_path def stop_discovery(self): self.discovering = False if self.Adapter is not None: self.Adapter.stop_discovery() def get_selected_device(self): selected = self.selected() if selected is not None: row = self.get(selected, "device") device = row["device"] return device def clear(self): if len(self.liststore): for i in self.liststore: tree_iter = i.iter device = self.get(tree_iter, "device")["device"] self.device_remove_event(device) self.liststore.clear() self.emit("device-selected", None, None) self.path_to_row = {} def find_device(self, device): object_path = device.get_object_path() try: row = self.path_to_row[object_path] if row.valid(): path = row.get_path() tree_iter = self.liststore.get_iter(path) return tree_iter else: del self.path_to_row[object_path] return None except KeyError: return None def find_device_by_path(self, path): try: row = self.path_to_row[path] if row.valid(): path = row.get_path() tree_iter = self.liststore.get_iter(path) return tree_iter else: del self.path_to_row[path] return None except KeyError: return None def do_cache(self, tree_iter, kwargs): object_path = None if "device" in kwargs: if kwargs["device"]: object_path = kwargs['device'].get_object_path() elif "dbus_path" in kwargs: if kwargs["dbus_path"]: object_path = kwargs['dbus_path'] else: existing = self.get(tree_iter, "dbus_path")["dbus_path"] if existing is not None: del self.path_to_row[existing] if object_path: logging.info(f"Caching new device {object_path}") self.path_to_row[object_path] = Gtk.TreeRowReference.new(self.liststore, self.liststore.get_path(tree_iter)) def append(self, **columns): tree_iter = GenericList.append(self, **columns) self.do_cache(tree_iter, columns) def prepend(self, **columns): tree_iter = GenericList.prepend(self, **columns) self.do_cache(tree_iter, columns) def set(self, tree_iter, **kwargs): GenericList.set(self, tree_iter, **kwargs) self.do_cache(tree_iter, kwargs)
class DeviceList(GenericList): __gsignals__: GSignals = { # @param: device TreeIter # note: None None is given when there ar no more rows, or when selected device is removed 'device-selected': (GObject.SignalFlags.RUN_LAST, None, ( Device, Gtk.TreeIter, )), # @param: device, TreeIter, (key, value) 'device-property-changed': (GObject.SignalFlags.RUN_LAST, None, ( Device, Gtk.TreeIter, object, )), # @param: adapter, (key, value) 'adapter-property-changed': (GObject.SignalFlags.RUN_LAST, None, ( Adapter, object, )), # @param: progress (0 to 1) 'discovery-progress': (GObject.SignalFlags.RUN_LAST, None, (float, )), # @param: new adapter path, None if there are no more adapters 'adapter-changed': (GObject.SignalFlags.RUN_LAST, None, (str, )), # @param: adapter path 'adapter-added': (GObject.SignalFlags.RUN_LAST, None, (str, )), 'adapter-removed': (GObject.SignalFlags.RUN_LAST, None, (str, )), } def __init__(self, adapter_name: Optional[str] = None, tabledata: Optional[List[ListDataDict]] = None, headers_visible: bool = True) -> None: if not tabledata: tabledata = [] # cache for fast lookup in the list self.path_to_row: Dict[str, Gtk.TreeRowReference] = {} self.manager = Manager() self._managerhandlers: List[int] = [] self._managerhandlers.append( self.manager.connect_signal('adapter-removed', self.__on_manager_signal, 'adapter-removed')) self._managerhandlers.append( self.manager.connect_signal('adapter-added', self.__on_manager_signal, 'adapter-added')) self._managerhandlers.append( self.manager.connect_signal('device-created', self.__on_manager_signal, 'device-created')) self._managerhandlers.append( self.manager.connect_signal('device-removed', self.__on_manager_signal, 'device-removed')) self.any_device = AnyDevice() self._anydevhandler = self.any_device.connect_signal( "property-changed", self._on_device_property_changed) self.__discovery_time: float = 0 self.__adapter_path: Optional[str] = None self.Adapter: Optional[Adapter] = None self.discovering = False data = tabledata + [{ "id": "device", "type": object }, { "id": "dbus_path", "type": str }, { "id": "timestamp", "type": float }] super().__init__(data, headers_visible=headers_visible) self.set_name("DeviceList") self.set_adapter(adapter_name) self._any_adapter = AnyAdapter() self._anyadapterhandler = self._any_adapter.connect_signal( "property-changed", self._on_property_changed) self._selectionhandler = self.selection.connect( 'changed', self.on_selection_changed) self.icon_theme = Gtk.IconTheme.get_default() self.icon_theme.prepend_search_path(ICON_PATH) # handle icon theme changes self.icon_theme.connect("changed", self.on_icon_theme_changed) def destroy(self) -> None: self.any_device.disconnect(self._anydevhandler) self._any_adapter.disconnect(self._anyadapterhandler) self.selection.disconnect(self._selectionhandler) for handler in self._managerhandlers: self.manager.disconnect(handler) super().destroy() def __on_manager_signal(self, _manager: Manager, path: str, signal_name: str) -> None: if signal_name == 'adapter-removed': self.emit("adapter-removed", path) if path == self.__adapter_path: self.clear() self.Adapter = None self.set_adapter() if signal_name == 'adapter-added': if self.Adapter is None: self.set_adapter(path) self.emit("adapter-added", path) if signal_name == 'device-created': tree_iter = self.find_device_by_path(path) if tree_iter is None: dev = Device(obj_path=path) self.device_add_event(dev) if signal_name == 'device-removed': tree_iter = self.find_device_by_path(path) if tree_iter: row = self.get(tree_iter, "device") dev = row["device"] self.device_remove_event(dev) def on_selection_changed(self, selection: Gtk.TreeSelection) -> None: _model, tree_iter = selection.get_selected() if tree_iter: row = self.get(tree_iter, "device") dev = row["device"] self.emit("device-selected", dev, tree_iter) def _on_property_changed(self, _adapter: AnyAdapter, key: str, value: object, path: str) -> None: if not self.Adapter or self.Adapter.get_object_path() != path: return if key == "Discovering" and not value: self.discovering = False self.emit("adapter-property-changed", self.Adapter, (key, value)) def _on_device_property_changed(self, _device: AnyDevice, key: str, value: object, path: str) -> None: tree_iter = self.find_device_by_path(path) if tree_iter is not None: dev = self.get(tree_iter, "device")["device"] self.row_update_event(tree_iter, key, value) self.emit("device-property-changed", dev, tree_iter, (key, value)) # Override when subclassing def on_icon_theme_changed(self, _icon_them: Gtk.IconTheme) -> None: logging.warning("Icons may not be updated with icon theme changes") # ##### virtual funcs ##### # called when row needs to be initialized def row_setup_event(self, tree_iter: Gtk.TreeIter, device: Device) -> None: pass # called when a property for a device changes def row_update_event(self, tree_iter: Gtk.TreeIter, key: str, value: Any) -> None: pass # called when device needs to be added to the list def device_add_event(self, device: Device) -> None: self.add_device(device) def device_remove_event(self, device: Device) -> None: logging.debug(device) tree_iter = self.find_device(device) assert tree_iter is not None if self.compare(self.selected(), tree_iter): self.emit("device-selected", None, None) self.delete(tree_iter) ######################### def set_adapter(self, adapter: Optional[str] = None) -> None: self.clear() if self.discovering: self.stop_discovery() self.emit("adapter-property-changed", self.Adapter, ("Discovering", False)) adapter = adapter_path_to_name(adapter) logging.debug(f"Setting adapter to: {adapter}") if adapter is not None: try: self.Adapter = self.manager.get_adapter(adapter) self.__adapter_path = self.Adapter.get_object_path() except DBusNoSuchAdapterError: logging.warning( 'Failed to set adapter, trying first available.') self.set_adapter(None) return else: adapters = self.manager.get_adapters() if len(adapters) > 0: self.Adapter = adapters[0] self.__adapter_path = self.Adapter.get_object_path() else: self.Adapter = None self.__adapter_path = None self.emit("adapter-changed", self.__adapter_path) def update_progress(self, time: float, totaltime: float) -> bool: if not self.discovering: return False self.__discovery_time += time progress = self.__discovery_time / totaltime if progress >= 1.0: progress = 1.0 if self.__discovery_time >= totaltime: self.stop_discovery() return False self.emit("discovery-progress", progress) return True def add_device(self, device: Device) -> None: # device belongs to another adapter if not self.Adapter or not device[ 'Adapter'] == self.Adapter.get_object_path(): return logging.info("adding new device") tree_iter = self.liststore.append() self.set(tree_iter, device=device) self.row_setup_event(tree_iter, device) object_path = device.get_object_path() timestamp = datetime.strftime(datetime.now(), '%Y%m%d%H%M%S%f') self.set(tree_iter, dbus_path=object_path, timestamp=float(timestamp)) def display_known_devices(self, autoselect: bool = False) -> None: self.clear() if self.Adapter: devices = self.manager.get_devices(self.Adapter.get_object_path()) for device in devices: self.device_add_event(device) if autoselect: self.selection.select_path(0) def discover_devices( self, time: float = 10.24, error_handler: Optional[Callable[[BluezDBusException], None]] = None ) -> None: if not self.discovering: self.__discovery_time = 0 if self.Adapter is not None: self.Adapter.start_discovery(error_handler=error_handler) self.discovering = True t = 1.0 / 15 * 1000 GLib.timeout_add(int(t), self.update_progress, t / 1000, time) def is_valid_adapter(self) -> bool: if self.Adapter is None: return False else: return True def get_adapter_path(self) -> Optional[str]: return self.__adapter_path if self.is_valid_adapter() else None def stop_discovery(self) -> None: self.discovering = False if self.Adapter is not None: self.Adapter.stop_discovery() def get_selected_device(self) -> Optional[Device]: selected = self.selected() if selected is not None: row = self.get(selected, "device") device: Device = row["device"] return device return None def clear(self) -> None: if len(self.liststore): for i in self.liststore: tree_iter = i.iter device = self.get(tree_iter, "device")["device"] self.device_remove_event(device) self.liststore.clear() self.emit("device-selected", None, None) self.path_to_row = {} def find_device(self, device: Device) -> Optional[Gtk.TreeIter]: object_path = device.get_object_path() try: row = self.path_to_row[object_path] if row.valid(): path = row.get_path() assert path is not None tree_iter = self.liststore.get_iter(path) return tree_iter else: del self.path_to_row[object_path] return None except KeyError: return None def find_device_by_path(self, path: str) -> Optional[Gtk.TreeIter]: try: row = self.path_to_row[path] if row.valid(): path_ = row.get_path() assert path_ is not None tree_iter = self.liststore.get_iter(path_) return tree_iter else: del self.path_to_row[path] return None except KeyError: return None def do_cache(self, tree_iter: Gtk.TreeIter, kwargs: Dict[str, Any]) -> None: object_path = None if "device" in kwargs: if kwargs["device"]: object_path = kwargs['device'].get_object_path() elif "dbus_path" in kwargs: if kwargs["dbus_path"]: object_path = kwargs['dbus_path'] else: existing = self.get(tree_iter, "dbus_path")["dbus_path"] if existing is not None: del self.path_to_row[existing] if object_path: logging.info(f"Caching new device {object_path}") self.path_to_row[object_path] = Gtk.TreeRowReference.new( self.liststore, self.liststore.get_path(tree_iter)) def append(self, **columns: object) -> Gtk.TreeIter: tree_iter = GenericList.append(self, **columns) self.do_cache(tree_iter, columns) return tree_iter def prepend(self, **columns: object) -> Gtk.TreeIter: tree_iter = GenericList.prepend(self, **columns) self.do_cache(tree_iter, columns) return tree_iter # FIXME: GenericList.set accepts int and str as iterid, DeviceList does not def set(self, iterid: Gtk.TreeIter, **kwargs: object) -> None: # type: ignore GenericList.set(self, iterid, **kwargs) self.do_cache(iterid, kwargs)