|
|
|
@ -69,8 +69,9 @@ class NetworkInfo: |
|
|
|
|
class WifiManagerCallbacks: |
|
|
|
|
need_auth: Callable[[str], None] | None = None |
|
|
|
|
activated: Callable[[], None] | None = None |
|
|
|
|
forgotten: Callable[[], None] | None = None |
|
|
|
|
forgotten: Callable[[str], None] | None = None |
|
|
|
|
networks_updated: Callable[[list[NetworkInfo]], None] | None = None |
|
|
|
|
connection_failed: Callable[[str, str], None] | None = None # Added for error feedback |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WifiManager: |
|
|
|
@ -98,8 +99,8 @@ class WifiManager: |
|
|
|
|
self.bus = await MessageBus(bus_type=BusType.SYSTEM).connect() |
|
|
|
|
if not await self._find_wifi_device(): |
|
|
|
|
raise ValueError("No Wi-Fi device found") |
|
|
|
|
await self._setup_signals(self.device_path) |
|
|
|
|
|
|
|
|
|
await self._setup_signals(self.device_path) |
|
|
|
|
self.active_ap_path = await self.get_active_access_point() |
|
|
|
|
await self.add_tethering_connection(self._tethering_ssid, DEFAULT_TETHERING_PASSWORD) |
|
|
|
|
self.saved_connections = await self._get_saved_connections() |
|
|
|
@ -122,7 +123,7 @@ class WifiManager: |
|
|
|
|
if self.bus: |
|
|
|
|
self.bus.disconnect() |
|
|
|
|
|
|
|
|
|
async def request_scan(self) -> None: |
|
|
|
|
async def _request_scan(self) -> None: |
|
|
|
|
try: |
|
|
|
|
interface = self.device_proxy.get_interface(NM_WIRELESS_IFACE) |
|
|
|
|
await interface.call_request_scan({}) |
|
|
|
@ -146,12 +147,23 @@ class WifiManager: |
|
|
|
|
try: |
|
|
|
|
nm_iface = await self._get_interface(NM, path, NM_CONNECTION_IFACE) |
|
|
|
|
await nm_iface.call_delete() |
|
|
|
|
|
|
|
|
|
if self._current_connection_ssid == ssid: |
|
|
|
|
self._current_connection_ssid = None |
|
|
|
|
|
|
|
|
|
if ssid in self.saved_connections: |
|
|
|
|
del self.saved_connections[ssid] |
|
|
|
|
|
|
|
|
|
for network in self.networks: |
|
|
|
|
if network.ssid == ssid: |
|
|
|
|
network.is_saved = False |
|
|
|
|
network.is_connected = False |
|
|
|
|
break |
|
|
|
|
|
|
|
|
|
# Notify UI of forgotten connection |
|
|
|
|
if self.callbacks.networks_updated: |
|
|
|
|
self.callbacks.networks_updated(copy.deepcopy(self.networks)) |
|
|
|
|
|
|
|
|
|
return True |
|
|
|
|
except DBusError as e: |
|
|
|
|
cloudlog.error(f"Failed to delete connection for SSID: {ssid}. Error: {e}") |
|
|
|
@ -212,10 +224,12 @@ class WifiManager: |
|
|
|
|
|
|
|
|
|
nm_iface = await self._get_interface(NM, NM_PATH, NM_IFACE) |
|
|
|
|
await nm_iface.call_add_and_activate_connection(connection, self.device_path, "/") |
|
|
|
|
await self._update_connection_status() |
|
|
|
|
except DBusError as e: |
|
|
|
|
except Exception as e: |
|
|
|
|
self._current_connection_ssid = None |
|
|
|
|
cloudlog.error(f"Error connecting to network: {e}") |
|
|
|
|
# Notify UI of failure |
|
|
|
|
if self.callbacks.connection_failed: |
|
|
|
|
self.callbacks.connection_failed(ssid, str(e)) |
|
|
|
|
|
|
|
|
|
def is_saved(self, ssid: str) -> bool: |
|
|
|
|
return ssid in self.saved_connections |
|
|
|
@ -393,8 +407,7 @@ class WifiManager: |
|
|
|
|
async def _periodic_scan(self): |
|
|
|
|
while self.running: |
|
|
|
|
try: |
|
|
|
|
await self.request_scan() |
|
|
|
|
await self._get_available_networks() |
|
|
|
|
await self._request_scan() |
|
|
|
|
await asyncio.sleep(30) |
|
|
|
|
except asyncio.CancelledError: |
|
|
|
|
break |
|
|
|
@ -423,21 +436,24 @@ class WifiManager: |
|
|
|
|
def _on_properties_changed(self, interface: str, changed: dict, invalidated: list): |
|
|
|
|
# print("property changed", interface, changed, invalidated) |
|
|
|
|
if 'LastScan' in changed: |
|
|
|
|
asyncio.create_task(self._get_available_networks()) |
|
|
|
|
asyncio.create_task(self._refresh_networks()) |
|
|
|
|
elif interface == NM_WIRELESS_IFACE and "ActiveAccessPoint" in changed: |
|
|
|
|
self.active_ap_path = changed["ActiveAccessPoint"].value |
|
|
|
|
asyncio.create_task(self._get_available_networks()) |
|
|
|
|
new_ap_path = changed["ActiveAccessPoint"].value |
|
|
|
|
if self.active_ap_path != new_ap_path: |
|
|
|
|
self.active_ap_path = new_ap_path |
|
|
|
|
asyncio.create_task(self._refresh_networks()) |
|
|
|
|
|
|
|
|
|
def _on_state_changed(self, new_state: int, old_state: int, reason: int): |
|
|
|
|
print(f"State changed: {old_state} -> {new_state}, reason: {reason}") |
|
|
|
|
print("State changed", new_state, old_state, reason) |
|
|
|
|
if new_state == NMDeviceState.ACTIVATED: |
|
|
|
|
if self.callbacks.activated: |
|
|
|
|
self.callbacks.activated() |
|
|
|
|
asyncio.create_task(self._update_connection_status()) |
|
|
|
|
asyncio.create_task(self._refresh_networks()) |
|
|
|
|
self._current_connection_ssid = None |
|
|
|
|
elif new_state in (NMDeviceState.DISCONNECTED, NMDeviceState.NEED_AUTH): |
|
|
|
|
for network in self.networks: |
|
|
|
|
network.is_connected = False |
|
|
|
|
|
|
|
|
|
if new_state == NMDeviceState.NEED_AUTH and reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT and self.callbacks.need_auth: |
|
|
|
|
if self._current_connection_ssid: |
|
|
|
|
self.callbacks.need_auth(self._current_connection_ssid) |
|
|
|
@ -453,19 +469,19 @@ class WifiManager: |
|
|
|
|
|
|
|
|
|
def _on_new_connection(self, path: str) -> None: |
|
|
|
|
"""Callback for NewConnection signal.""" |
|
|
|
|
print(f"New connection added: {path}") |
|
|
|
|
asyncio.create_task(self._add_saved_connection(path)) |
|
|
|
|
|
|
|
|
|
def _on_connection_removed(self, path: str) -> None: |
|
|
|
|
"""Callback for ConnectionRemoved signal.""" |
|
|
|
|
print(f"Connection removed: {path}") |
|
|
|
|
for ssid, p in list(self.saved_connections.items()): |
|
|
|
|
if path == p: |
|
|
|
|
del self.saved_connections[ssid] |
|
|
|
|
|
|
|
|
|
if self.callbacks.forgotten: |
|
|
|
|
self.callbacks.forgotten() |
|
|
|
|
self.callbacks.forgotten(ssid) |
|
|
|
|
|
|
|
|
|
# Update network list to reflect the removed saved connection |
|
|
|
|
asyncio.create_task(self._update_connection_status()) |
|
|
|
|
asyncio.create_task(self._refresh_networks()) |
|
|
|
|
break |
|
|
|
|
|
|
|
|
|
async def _add_saved_connection(self, path: str) -> None: |
|
|
|
@ -474,7 +490,7 @@ class WifiManager: |
|
|
|
|
settings = await self._get_connection_settings(path) |
|
|
|
|
if ssid := self._extract_ssid(settings): |
|
|
|
|
self.saved_connections[ssid] = path |
|
|
|
|
await self._update_connection_status() |
|
|
|
|
await self._refresh_networks() |
|
|
|
|
except DBusError as e: |
|
|
|
|
cloudlog.error(f"Failed to add connection {path}: {e}") |
|
|
|
|
|
|
|
|
@ -483,10 +499,6 @@ class WifiManager: |
|
|
|
|
ssid_variant = settings.get('802-11-wireless', {}).get('ssid', Variant('ay', b'')).value |
|
|
|
|
return ''.join(chr(b) for b in ssid_variant) if ssid_variant else None |
|
|
|
|
|
|
|
|
|
async def _update_connection_status(self): |
|
|
|
|
self.active_ap_path = await self.get_active_access_point() |
|
|
|
|
await self._get_available_networks() |
|
|
|
|
|
|
|
|
|
async def _add_match_rule(self, rule): |
|
|
|
|
"""Add a match rule on the bus.""" |
|
|
|
|
reply = await self.bus.call( |
|
|
|
@ -504,10 +516,11 @@ class WifiManager: |
|
|
|
|
assert reply.message_type == MessageType.METHOD_RETURN |
|
|
|
|
return reply |
|
|
|
|
|
|
|
|
|
async def _get_available_networks(self): |
|
|
|
|
async def _refresh_networks(self): |
|
|
|
|
"""Get a list of available networks via NetworkManager.""" |
|
|
|
|
wifi_iface = self.device_proxy.get_interface(NM_WIRELESS_IFACE) |
|
|
|
|
access_points = await wifi_iface.get_access_points() |
|
|
|
|
self.active_ap_path = await self.get_active_access_point() |
|
|
|
|
network_dict = {} |
|
|
|
|
for ap_path in access_points: |
|
|
|
|
try: |
|
|
|
@ -531,7 +544,7 @@ class WifiManager: |
|
|
|
|
security_type=self._get_security_type(flags, wpa_flags, rsn_flags), |
|
|
|
|
path=ap_path, |
|
|
|
|
bssid=bssid, |
|
|
|
|
is_connected=self.active_ap_path == ap_path, |
|
|
|
|
is_connected=self.active_ap_path == ap_path and self._current_connection_ssid != ssid, |
|
|
|
|
is_saved=ssid in self.saved_connections |
|
|
|
|
) |
|
|
|
|
|
|
|
|
@ -555,9 +568,7 @@ class WifiManager: |
|
|
|
|
async def _get_connection_settings(self, path): |
|
|
|
|
"""Fetch connection settings for a specific connection path.""" |
|
|
|
|
try: |
|
|
|
|
connection_proxy = await self.bus.introspect(NM, path) |
|
|
|
|
connection = self.bus.get_proxy_object(NM, path, connection_proxy) |
|
|
|
|
settings = connection.get_interface(NM_CONNECTION_IFACE) |
|
|
|
|
settings = await self._get_interface(NM, path, NM_CONNECTION_IFACE) |
|
|
|
|
return await settings.call_get_settings() |
|
|
|
|
except DBusError as e: |
|
|
|
|
cloudlog.error(f"Failed to get settings for {path}: {str(e)}") |
|
|
|
@ -660,12 +671,6 @@ class WifiManagerWrapper: |
|
|
|
|
return |
|
|
|
|
self._run_coroutine(self._manager.connect()) |
|
|
|
|
|
|
|
|
|
def request_scan(self): |
|
|
|
|
"""Request a scan for Wi-Fi networks.""" |
|
|
|
|
if not self._manager: |
|
|
|
|
return |
|
|
|
|
self._run_coroutine(self._manager.request_scan()) |
|
|
|
|
|
|
|
|
|
def forget_connection(self, ssid: str): |
|
|
|
|
"""Forget a saved Wi-Fi connection.""" |
|
|
|
|
if not self._manager: |
|
|
|
|