From 39e42db6f6f6eb470105d837e544983e9c33f1cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Fri, 21 Aug 2026 14:57:04 +0000 Subject: [PATCH 1/7] core: services: wifi: WifiManager: Start without a wlan interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- .../wpa_supplicant/WifiManager.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/core/services/wifi/wifi_handlers/wpa_supplicant/WifiManager.py b/core/services/wifi/wifi_handlers/wpa_supplicant/WifiManager.py index b6eadef26e..88bf5ebb9e 100644 --- a/core/services/wifi/wifi_handlers/wpa_supplicant/WifiManager.py +++ b/core/services/wifi/wifi_handlers/wpa_supplicant/WifiManager.py @@ -192,6 +192,8 @@ def hotspot(self) -> HotspotManager: async def get_wifi_available(self) -> List[ScannedWifiNetwork]: """Get a dict from the wifi signals available""" + if self.wpa_path is None: + return [] async def perform_new_scan() -> None: try: @@ -231,6 +233,8 @@ async def perform_new_scan() -> None: async def get_saved_wifi_network(self) -> List[SavedWifiNetwork]: """Get a list of saved wifi networks""" + if self.wpa_path is None: + return [] try: data = await self.wpa.send_command_list_networks() networks_list = WifiManager.__dict_from_table(data) @@ -331,6 +335,8 @@ async def connect_to_network(self, network_id: int, timeout: float = 20.0) -> No async def status(self) -> WifiStatus: """Check wpa_supplicant status""" + if self.wpa_path is None: + return WifiStatus(state="unavailable") try: data = await self.wpa.send_command_status() return WifiStatus(**WifiManager.__dict_from_list(data)) @@ -473,6 +479,9 @@ async def set_hotspot_credentials(self, credentials: WifiCredentials) -> None: self._settings_manager.settings.hotspot_password = credentials.password self._settings_manager.save() + if self.wpa_path is None: + return + self.hotspot.set_credentials(credentials) if self.hotspot.is_running(): @@ -481,6 +490,9 @@ async def set_hotspot_credentials(self, credentials: WifiCredentials) -> None: await self.enable_hotspot(save_settings=False) def hotspot_credentials(self) -> WifiCredentials: + if self.wpa_path is None: + settings = self._settings_manager.settings + return WifiCredentials(ssid=settings.hotspot_ssid or "", password=settings.hotspot_password or "") credentials: WifiCredentials = self.hotspot.credentials return credentials @@ -567,14 +579,19 @@ def is_socket(file_path: str) -> bool: try: await self.connect(("127.0.0.1", 6664)) except Exception as udp_connection_error: - logger.error(f"Could not connect with internet socket: {udp_connection_error}. Exiting.") - raise udp_connection_error + logger.error(f"Could not connect with internet socket: {udp_connection_error}.") + self.wpa_path = None + return loop = asyncio.get_event_loop() loop.create_task(self.auto_reconnect(60)) loop.create_task(self.start_hotspot_watchdog()) async def supports_hotspot(self) -> bool: + if self.wpa_path is None: + return False return self.hotspot.supports_hotspot async def hotspot_is_running(self) -> bool: + if self.wpa_path is None: + return False return self.hotspot.is_running() From ebe28dd6486517a6f0e470d92fa082741a0c6fb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Fri, 21 Aug 2026 14:57:04 +0000 Subject: [PATCH 2/7] core: frontend: components: wifi: WifiUpdater: Keep polling if wifi is gone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- core/frontend/src/components/wifi/WifiUpdater.vue | 9 +++++++++ core/frontend/src/types/wifi.ts | 1 + 2 files changed, 10 insertions(+) diff --git a/core/frontend/src/components/wifi/WifiUpdater.vue b/core/frontend/src/components/wifi/WifiUpdater.vue index 8187640f5c..4adfc8ced8 100644 --- a/core/frontend/src/components/wifi/WifiUpdater.vue +++ b/core/frontend/src/components/wifi/WifiUpdater.vue @@ -40,6 +40,15 @@ export default Vue.extend({ timeout: 10000, }) .then((response) => { + if (response.data.state === 'unavailable') { + wifi.setNetworkStatus(response.data) + wifi.setCurrentNetwork(null) + wifi.setAvailableNetworks([]) + this.fetch_network_status_task.setDelay(30000) + return + } + + this.fetch_network_status_task.setDelay(5000) wifi.setNetworkStatus(response.data) if (response.data.wpa_state !== 'COMPLETED') { diff --git a/core/frontend/src/types/wifi.ts b/core/frontend/src/types/wifi.ts index c5ddf67615..a88784d371 100644 --- a/core/frontend/src/types/wifi.ts +++ b/core/frontend/src/types/wifi.ts @@ -27,6 +27,7 @@ export interface WifiStatus { p2p_device_address: string address: string uuid: string + state?: string } export interface WPANetwork { From d264181d01d26dcc76edf44f9b5270b4f1e197c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Fri, 21 Aug 2026 14:57:04 +0000 Subject: [PATCH 3/7] core: frontend: components: wifi: WifiManager: Say when there is no adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- core/frontend/src/components/wifi/WifiManager.vue | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/core/frontend/src/components/wifi/WifiManager.vue b/core/frontend/src/components/wifi/WifiManager.vue index 5698353222..e94d49b0e0 100644 --- a/core/frontend/src/components/wifi/WifiManager.vue +++ b/core/frontend/src/components/wifi/WifiManager.vue @@ -80,8 +80,13 @@ flat class="text-body-1 text-center" > - No wifi networks available :(
- Rescanning... + +
From 9b4a67eefeabda04eade24c224f63afc9487a12a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Thu, 10 Sep 2026 09:31:30 -0300 Subject: [PATCH 4/7] core: services: wifi: WifiManager: Move _hotspot to a class attribute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The attribute was only created inside connect, which never runs when there is no socket to talk to, so the hotspot property raised AttributeError instead of building the manager. Signed-off-by: Patrick José Pereira --- core/services/wifi/wifi_handlers/wpa_supplicant/WifiManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/services/wifi/wifi_handlers/wpa_supplicant/WifiManager.py b/core/services/wifi/wifi_handlers/wpa_supplicant/WifiManager.py index 88bf5ebb9e..d036dc1a03 100644 --- a/core/services/wifi/wifi_handlers/wpa_supplicant/WifiManager.py +++ b/core/services/wifi/wifi_handlers/wpa_supplicant/WifiManager.py @@ -28,6 +28,7 @@ class WifiManager(AbstractWifiManager): wpa = WPASupplicant() wpa_path: Optional[str] = None + _hotspot: Optional[HotspotManager] = None async def can_work(self) -> bool: return bool(get_host_os() == HostOs.Bullseye) @@ -104,7 +105,6 @@ async def connect(self, path: Any) -> None: await self.get_wifi_available() try: - self._hotspot: Optional[HotspotManager] = None ssid, password = ( self._settings_manager.settings.hotspot_ssid, self._settings_manager.settings.hotspot_password, From e93805237c9527871e3e7ba07552cb7c21a30ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Thu, 10 Sep 2026 09:32:55 -0300 Subject: [PATCH 5/7] core: services: wifi: WifiManager: Set wpa_path after the socket connects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It was set before the connection was attempted, so the udp fallback working was reported to the frontend as no wifi adapter. Signed-off-by: Patrick José Pereira --- core/services/wifi/wifi_handlers/wpa_supplicant/WifiManager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/services/wifi/wifi_handlers/wpa_supplicant/WifiManager.py b/core/services/wifi/wifi_handlers/wpa_supplicant/WifiManager.py index d036dc1a03..ee148d561e 100644 --- a/core/services/wifi/wifi_handlers/wpa_supplicant/WifiManager.py +++ b/core/services/wifi/wifi_handlers/wpa_supplicant/WifiManager.py @@ -95,6 +95,8 @@ async def connect(self, path: Any) -> None: path {[tuple/str]} -- Can be a tuple to connect (ip/port) or unix socket file """ self.wpa.run(path) + # run only returns when the socket is connected, before that there is no adapter to talk to + self.wpa_path = str(path) self._scan_task: Optional[asyncio.Task[bytes]] = None self._updated_scan_results: Optional[List[ScannedWifiNetwork]] = None self._ignored_reconnection_networks: List[str] = [] @@ -571,7 +573,6 @@ def is_socket(file_path: str) -> bool: socket_name = available_sockets[-1] logger.info(f"Going to use {socket_name} file") WLAN_SOCKET = os.path.join(wpa_socket_folder, socket_name) - self.wpa_path = WLAN_SOCKET await self.connect(WLAN_SOCKET) except Exception as socket_connection_error: logger.warning(f"Could not connect with wifi socket. {socket_connection_error}") From e72d9671d1fba7671b6254b1f4837e3bae8bee31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Thu, 10 Sep 2026 09:33:03 -0300 Subject: [PATCH 6/7] core: services: wifi: networkmanager: Report unavailable with no adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both handlers use the same state now, the frontend says there is no adapter on bookworm as well. Signed-off-by: Patrick José Pereira --- .../wifi/wifi_handlers/networkmanager/networkmanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/services/wifi/wifi_handlers/networkmanager/networkmanager.py b/core/services/wifi/wifi_handlers/networkmanager/networkmanager.py index 69293efb4b..d4ee082f45 100644 --- a/core/services/wifi/wifi_handlers/networkmanager/networkmanager.py +++ b/core/services/wifi/wifi_handlers/networkmanager/networkmanager.py @@ -420,7 +420,7 @@ async def supports_hotspot(self) -> bool: async def status(self) -> WifiStatus: if not self._device_path: - return WifiStatus(state="disconnected") + return WifiStatus(state="unavailable") device = NetworkDeviceWireless(self._device_path, self._bus) state = await device.state From 47efdf594917dcf97d180ef7b2e7516feeeab477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Jos=C3=A9=20Pereira?= Date: Thu, 10 Sep 2026 09:33:03 -0300 Subject: [PATCH 7/7] core: services: wifi: test_wifi_manager: First version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrick José Pereira --- core/services/wifi/test_wifi_manager.py | 46 +++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 core/services/wifi/test_wifi_manager.py diff --git a/core/services/wifi/test_wifi_manager.py b/core/services/wifi/test_wifi_manager.py new file mode 100644 index 0000000000..783018fe0f --- /dev/null +++ b/core/services/wifi/test_wifi_manager.py @@ -0,0 +1,46 @@ +import sys +from pathlib import Path +from typing import Any + +import pytest + +_WIFI_DIR = str(Path(__file__).resolve().parent) +sys.path.insert(0, _WIFI_DIR) +_saved = {name: sys.modules.get(name) for name in ("exceptions", "settings", "typedefs")} +for name in _saved: + sys.modules.pop(name, None) + +from wifi_handlers.wpa_supplicant.WifiManager import WifiManager + +for name, module in _saved.items(): + if module is not None: + sys.modules[name] = module + else: + sys.modules.pop(name, None) + +pytestmark = pytest.mark.asyncio + + +async def test_reports_unavailable_without_a_socket() -> None: + manager = WifiManager.__new__(WifiManager) + + assert (await manager.status()).state == "unavailable" + assert await manager.get_wifi_available() == [] + assert await manager.get_saved_wifi_network() == [] + assert await manager.supports_hotspot() is False + assert await manager.hotspot_is_running() is False + + +async def test_reports_available_after_connecting(monkeypatch: pytest.MonkeyPatch) -> None: + manager = WifiManager.__new__(WifiManager) + monkeypatch.setattr(manager.wpa, "run", lambda _target: None) + monkeypatch.setattr(WifiManager, "get_wifi_available", _no_networks) + + # The udp socket is the fallback when there is no wpa_supplicant socket, it still means wifi works + await manager.connect(("127.0.0.1", 6664)) + + assert manager.wpa_path is not None + + +async def _no_networks(_self: Any) -> list[Any]: + return []