fix(network): detect usb and bluetooth tethering in panel
Wired devices were tracked as a single slot, so USB tethering on a second port never got an Ethernet service object and the indicator kept showing wifi-off while online. Track all wired devices like wifi devices, prefer the one with an active connection, treat BT PAN (type 5) as wired, and refresh on device removal.
This commit is contained in:
2 files changed
+92
-57
No files matched your search
+37
-10
@@ -27,6 +27,8 @@ NM_CONN_SETTINGS_IFACE = "org.freedesktop.NetworkManager.Settings.Connection"
|
||||
# DeviceType constants
|
||||
DEVICE_TYPE_ETHERNET = 1
|
||||
DEVICE_TYPE_WIFI = 2
|
||||
# Bluetooth PAN (e.g. bluetooth tethering) shows up as its own device type
|
||||
DEVICE_TYPE_BLUETOOTH = 5
|
||||
|
||||
# DeviceState constants
|
||||
DEVICE_STATE_UNMANAGED = 10
|
||||
@@ -759,10 +761,17 @@ class Ethernet(Service):
|
||||
@Signal
|
||||
def changed(self) -> None: ...
|
||||
|
||||
def __init__(self, nm: NmProxy, device_path: str, **kwargs) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
nm: NmProxy,
|
||||
device_path: str,
|
||||
device_type: int = DEVICE_TYPE_ETHERNET,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self._nm = nm
|
||||
self._device_path = device_path
|
||||
self.device_type = device_type
|
||||
self._handler_id: int | None = None
|
||||
dev = _make_proxy(nm._bus, device_path, NM_DEVICE_IFACE)
|
||||
self._handler_id = dev.connect(
|
||||
@@ -835,6 +844,7 @@ class NetworkClient(Service):
|
||||
self.wifi_device: Wifi | None = None
|
||||
self.wifi_devices: dict[str, Wifi] = {}
|
||||
self.ethernet_device: Ethernet | None = None
|
||||
self.ethernet_devices: dict[str, Ethernet] = {}
|
||||
self._airplane_mode: bool = False
|
||||
self._nm_proxy_handler: int | None = None
|
||||
super().__init__(**kwargs)
|
||||
@@ -872,8 +882,7 @@ class NetworkClient(Service):
|
||||
new_devices = set(self._nm.devices)
|
||||
# Track by old wifi device paths
|
||||
old_paths = {w._device_path for w in self.wifi_devices.values()}
|
||||
if self.ethernet_device:
|
||||
old_paths.add(self.ethernet_device._device_path)
|
||||
old_paths |= set(self.ethernet_devices.keys())
|
||||
|
||||
new_paths = set(new_devices)
|
||||
added = new_paths - old_paths
|
||||
@@ -901,9 +910,12 @@ class NetworkClient(Service):
|
||||
self.emit("device-added", iface)
|
||||
self.emit("device-ready")
|
||||
|
||||
elif dtype == DEVICE_TYPE_ETHERNET:
|
||||
if self.ethernet_device is None:
|
||||
self.ethernet_device = Ethernet(self._nm, dev_path)
|
||||
elif dtype in (DEVICE_TYPE_ETHERNET, DEVICE_TYPE_BLUETOOTH):
|
||||
if dev_path not in self.ethernet_devices:
|
||||
ethernet = Ethernet(self._nm, dev_path, device_type=dtype)
|
||||
self.ethernet_devices[dev_path] = ethernet
|
||||
if self.ethernet_device is None:
|
||||
self.ethernet_device = ethernet
|
||||
self.emit("device-ready")
|
||||
|
||||
def _handle_device_removed(self, dev_path: str):
|
||||
@@ -922,16 +934,31 @@ class NetworkClient(Service):
|
||||
logger.info(f"[Network] Wifi device removed: {iface}")
|
||||
self.emit("device-removed", iface)
|
||||
|
||||
elif dtype == DEVICE_TYPE_ETHERNET:
|
||||
if self.ethernet_device is not None:
|
||||
self.ethernet_device.close()
|
||||
self.ethernet_device = None
|
||||
elif dtype in (DEVICE_TYPE_ETHERNET, DEVICE_TYPE_BLUETOOTH):
|
||||
ethernet = self.ethernet_devices.pop(dev_path, None)
|
||||
if ethernet is not None:
|
||||
ethernet.close()
|
||||
if self.ethernet_device is ethernet:
|
||||
self.ethernet_device = next(
|
||||
iter(self.ethernet_devices.values()), None
|
||||
)
|
||||
self.emit("device-removed", iface)
|
||||
|
||||
# Drop cached proxies now that signal handlers are disconnected and the
|
||||
# removed device is no longer referenced.
|
||||
_invalidate_proxy(dev_path)
|
||||
|
||||
def get_ethernet_device(self) -> Ethernet | None:
|
||||
"""Return the wired device with an active connection if any.
|
||||
|
||||
USB tethering appears as an extra wired device, so prefer one that is
|
||||
activated/activating over a merely present (often unavailable) port.
|
||||
"""
|
||||
for ethernet in self.ethernet_devices.values():
|
||||
if ethernet.internet in ("activated", "activating"):
|
||||
return ethernet
|
||||
return next(iter(self.ethernet_devices.values()), None)
|
||||
|
||||
def _get_primary_device(self) -> Literal["wifi", "wired"] | None:
|
||||
if not self._nm:
|
||||
return None
|
||||
|
||||
@@ -5,7 +5,7 @@ from fabric.widgets.label import Label
|
||||
|
||||
from services.battery import Battery
|
||||
from services.bluetooth import BluetoothClient
|
||||
from services.network import NetworkClient
|
||||
from services.network import DEVICE_TYPE_BLUETOOTH, NetworkClient
|
||||
from shared.window.applet_window import AppletWindow
|
||||
from shared.window.battery_widget import BatteryControl
|
||||
from utils.functions import format_duration, get_wifi_icon_for_strength
|
||||
@@ -168,6 +168,7 @@ class NetworkIndicator(Box):
|
||||
self.show_window = show_window
|
||||
|
||||
self.network_service = NetworkClient()
|
||||
self._connected_eth_paths: set[str] = set()
|
||||
|
||||
self.network_icon = svg_file("applets/wifi-clear.svg", size=22)
|
||||
|
||||
@@ -200,6 +201,7 @@ class NetworkIndicator(Box):
|
||||
modus_service.connect("wlan-changed", self.on_wlan_changed)
|
||||
self.network_service.connect("device-ready", self.on_wifi_device_added)
|
||||
self.network_service.connect("device-ready", self.on_ethernet_device_added)
|
||||
self.network_service.connect("device-removed", self.on_network_changed)
|
||||
self.update_modus_service_wlan_state()
|
||||
self.update_state()
|
||||
|
||||
@@ -216,10 +218,12 @@ class NetworkIndicator(Box):
|
||||
self.update_state()
|
||||
|
||||
def on_ethernet_device_added(self, *args):
|
||||
if self.network_service.ethernet_device:
|
||||
self.network_service.ethernet_device.connect(
|
||||
"changed", self.on_network_direct_changed
|
||||
)
|
||||
# Track every wired device (e.g. USB tethering adds one) so any of
|
||||
# them can drive the indicator state
|
||||
for path, eth in self.network_service.ethernet_devices.items():
|
||||
if path not in self._connected_eth_paths:
|
||||
eth.connect("changed", self.on_network_direct_changed)
|
||||
self._connected_eth_paths.add(path)
|
||||
self.update_modus_service_wlan_state()
|
||||
self.update_state()
|
||||
|
||||
@@ -236,27 +240,25 @@ class NetworkIndicator(Box):
|
||||
|
||||
if self.network_service.airplane_mode:
|
||||
wlan_state = "airplane"
|
||||
elif self.network_service.wifi_device:
|
||||
else:
|
||||
wifi = self.network_service.wifi_device
|
||||
if not wifi.enabled:
|
||||
wlan_state = "disabled"
|
||||
elif wifi.ssid and wifi.ssid != "Disconnected":
|
||||
ethernet = self.network_service.get_ethernet_device()
|
||||
|
||||
# Ethernet (including USB tethering) must be considered even when
|
||||
# a disconnected wifi device is present
|
||||
if wifi and wifi.enabled and wifi.ssid and wifi.ssid != "Disconnected":
|
||||
wlan_state = f"connected:{wifi.ssid}"
|
||||
if wifi.strength >= 0:
|
||||
wlan_state += f":{wifi.strength}%"
|
||||
else:
|
||||
wlan_state = "enabled"
|
||||
|
||||
# Check Ethernet if WiFi is not connected
|
||||
elif self.network_service.ethernet_device:
|
||||
ethernet = self.network_service.ethernet_device
|
||||
if ethernet.internet == "activated":
|
||||
elif ethernet and ethernet.internet == "activated":
|
||||
wlan_state = "ethernet:connected"
|
||||
if hasattr(ethernet, "speed") and ethernet.speed:
|
||||
wlan_state += f":{ethernet.speed}"
|
||||
elif ethernet.internet == "activating":
|
||||
elif ethernet and ethernet.internet == "activating":
|
||||
wlan_state = "ethernet:connecting"
|
||||
else:
|
||||
elif wifi:
|
||||
wlan_state = "enabled" if wifi.enabled else "disabled"
|
||||
elif ethernet:
|
||||
wlan_state = "ethernet:disconnected"
|
||||
|
||||
modus_service.wlan = wlan_state
|
||||
@@ -265,17 +267,18 @@ class NetworkIndicator(Box):
|
||||
tooltip = "No network connection"
|
||||
icon_file = "wifi-off-clear.svg"
|
||||
|
||||
# Airplane mode takes priority
|
||||
if self.network_service.airplane_mode:
|
||||
icon_file = "wifi-off-clear.svg"
|
||||
# Airplane mode takes priority
|
||||
tooltip = "Airplane mode"
|
||||
# Check WiFi first (prioritize WiFi over Ethernet)
|
||||
elif self.network_service.wifi_device:
|
||||
else:
|
||||
wifi = self.network_service.wifi_device
|
||||
if not wifi.enabled:
|
||||
icon_file = "wifi-off-clear.svg"
|
||||
tooltip = "WiFi disabled"
|
||||
elif wifi.ssid and wifi.ssid != "Disconnected":
|
||||
ethernet = self.network_service.get_ethernet_device()
|
||||
wifi_connected = (
|
||||
wifi and wifi.enabled and wifi.ssid and wifi.ssid != "Disconnected"
|
||||
)
|
||||
|
||||
if wifi_connected:
|
||||
# WiFi takes priority when actually connected
|
||||
wifi_icon_path = get_wifi_icon_for_strength(wifi.strength)
|
||||
self.network_icon.set_from_file(wifi_icon_path)
|
||||
tooltip = f"Connected to {wifi.ssid}"
|
||||
@@ -283,22 +286,27 @@ class NetworkIndicator(Box):
|
||||
tooltip += f" ({wifi.strength}%)"
|
||||
self.network_button.set_tooltip_text(tooltip)
|
||||
return # Early return to avoid setting icon again
|
||||
else:
|
||||
icon_file = "wifi-off-clear.svg"
|
||||
tooltip = "WiFi disconnected"
|
||||
|
||||
# Check Ethernet if WiFi is not connected
|
||||
elif self.network_service.ethernet_device:
|
||||
ethernet = self.network_service.ethernet_device
|
||||
if ethernet.internet == "activated":
|
||||
# Check Ethernet regardless of WiFi presence. USB tethering shows
|
||||
# up as a wired device, so it would be masked by the elif before.
|
||||
if ethernet and ethernet.internet in ("activated", "activating"):
|
||||
icon_file = "network-wired.svg"
|
||||
tooltip = "Ethernet connected"
|
||||
if hasattr(ethernet, "speed") and ethernet.speed:
|
||||
tooltip += f" ({ethernet.speed})"
|
||||
elif ethernet.internet == "activating":
|
||||
icon_file = "network-wired.svg"
|
||||
tooltip = "Ethernet connecting..."
|
||||
else:
|
||||
wired_label = (
|
||||
"Bluetooth tethering"
|
||||
if ethernet.device_type == DEVICE_TYPE_BLUETOOTH
|
||||
else "Ethernet"
|
||||
)
|
||||
if ethernet.internet == "activated":
|
||||
tooltip = f"{wired_label} connected"
|
||||
if hasattr(ethernet, "speed") and ethernet.speed:
|
||||
tooltip += f" ({ethernet.speed})"
|
||||
else:
|
||||
tooltip = f"{wired_label} connecting..."
|
||||
elif wifi and not wifi.enabled:
|
||||
tooltip = "WiFi disabled"
|
||||
elif wifi:
|
||||
tooltip = "WiFi disconnected"
|
||||
elif ethernet:
|
||||
icon_file = "network-wired-offline.svg"
|
||||
tooltip = "Ethernet disconnected"
|
||||
|
||||
@@ -535,14 +543,14 @@ def add_destroy_to_indicators():
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred: {e}")
|
||||
if (
|
||||
hasattr(self.network_service, "ethernet_device")
|
||||
and self.network_service.ethernet_device
|
||||
):
|
||||
for eth_path in self._connected_eth_paths:
|
||||
eth = getattr(self.network_service, "ethernet_devices", {}).get(
|
||||
eth_path
|
||||
)
|
||||
if eth is None:
|
||||
continue
|
||||
try:
|
||||
self.network_service.ethernet_device.disconnect_by_func(
|
||||
self.on_network_direct_changed
|
||||
)
|
||||
eth.disconnect_by_func(self.on_network_direct_changed)
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred: {e}")
|
||||
except Exception as e:
|
||||
|
||||
Reference in new issue
Block a user