Skip to content

Conversation

@patrickelectric
Copy link
Member

@patrickelectric patrickelectric commented Jan 21, 2026

Summary by Sourcery

Add multi-interface WiFi support across backend and frontend while preserving backward-compatible single-interface behavior.

New Features:

  • Expose a v2 WiFi API that supports listing interfaces, per-interface scans, status, connect, disconnect, and shared saved networks.
  • Add NetworkManager-based handling of multiple WiFi interfaces, including per-interface connection management and hotspot handling tied to specific interfaces.
  • Introduce frontend components and store state to manage and visualize multiple WiFi interfaces, including per-interface tray icons, managers, and connection dialogs.
  • Display WiFi frequency band information in the network card UI and improve signal icon handling for both percentage and dBm values.

Enhancements:

  • Refine autoscan and hotspot management to operate correctly in a multi-interface environment while defaulting to wlan0 for hotspot and maintaining existing flows.
  • Generalize WiFi handling in the wpa_supplicant manager to use configurable interfaces instead of hardcoded wlan0.
  • Adjust main dashboard widget layout to show one WiFi widget per detected interface with a fallback to the legacy single-interface widget.

Adds support for multiple WiFi interfaces (wlan0, wlan1, etc.):
- New API v2 endpoints: /interfaces/, /wifi/scan, /wifi/status, /wifi/connect
- NetworkManager handler discovers all WiFi devices dynamically
- Filter uap0 virtual interface from user-selectable interfaces
- Hotspot operations remain bound to wlan0
- Use sys.modules pattern in routers to avoid circular imports

Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
- WifiInterfaceTrayMenu: Per-interface icon with dropdown
- WifiInterfaceManager: Network list and connection UI per interface
- InterfaceConnectionDialog: Connection dialog for v2 API
- WifiNetworkCard: Add 2.4G/5G/6G frequency band chip
- WifiTrayMenu: Show icon for each detected interface
- WifiUpdater: Poll v2 API endpoints for interface data

Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
…rength

NetworkManager returns percentage (0-100), wpa_supplicant returns dBm (negative).

Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
@patrickelectric patrickelectric marked this pull request as draft January 21, 2026 17:01
@sourcery-ai
Copy link

sourcery-ai bot commented Jan 21, 2026

Reviewer's Guide

Implements multi-interface WiFi support end‑to‑end by extending the NetworkManager backend, introducing a versioned v2 REST API, and updating the frontend to manage and visualize multiple WiFi interfaces while keeping existing v1 behavior intact.

Class diagram for multi-interface backend WiFi models and manager

classDiagram
  class WifiCredentials {
    +str ssid
    +str password
  }

  class ScannedWifiNetwork {
    +str ssid
    +int signallevel
    +int frequency
    +str bssid
    +str flags
  }

  class SavedWifiNetwork {
    +str ssid
    +str nm_uuid
  }

  class WifiInterface {
    +str name
    +bool connected
    +str ssid
    +int signal_strength
    +str ip_address
    +str mac_address
  }

  class WifiInterfaceStatus {
    +str interface
    +str state
    +str ssid
    +str bssid
    +str ip_address
    +int signal_strength
    +int frequency
    +str key_mgmt
  }

  class WifiInterfaceScanResult {
    +str interface
    +List~ScannedWifiNetwork~ networks
  }

  class WifiInterfaceList {
    +List~WifiInterface~ interfaces
    +str hotspot_interface
  }

  class ConnectRequest {
    +str interface
    +WifiCredentials credentials
    +bool hidden
  }

  class DisconnectRequest {
    +str interface
  }

  class AbstractWifiManager {
    <<abstract>>
    +async status() WifiStatus
    +async get_wifi_available() List~ScannedWifiNetwork~
    +async try_connect_to_network(credentials, hidden)
    +async disconnect()
    +async get_saved_wifi_network() List~SavedWifiNetwork~
    +async remove_network(ssid)
  }

  class NetworkManagerWifi {
    +Dict~str,str~ _device_paths
    +str _device_path
    +str _ap_interface
    +async start()
    +async _get_wifi_devices() Dict~str,str~
    +async _create_virtual_interface() bool
    +async _autoscan()
    +async get_wifi_interfaces() List~WifiInterface~
    +async get_interface_status(interface_name) WifiInterface
    +async scan_interface(interface_name) List~ScannedWifiNetwork~
    +async scan_all_interfaces() List~WifiInterfaceScanResult~
    +async _scan_device(device_path) List~ScannedWifiNetwork~
    +async get_interface_connection_status(interface_name) WifiInterfaceStatus
    +async get_all_interface_status() List~WifiInterfaceStatus~
    +async connect_interface(interface_name, credentials, hidden)
    +async disconnect_interface(interface_name)
    +async get_wifi_available() List~ScannedWifiNetwork~
    +async try_connect_to_network(credentials, hidden)
  }

  AbstractWifiManager <|-- NetworkManagerWifi

  WifiInterfaceScanResult "*" --> "*" ScannedWifiNetwork
  WifiInterfaceList "*" --> "*" WifiInterface
  ConnectRequest --> WifiCredentials

  class WifiRouterV2 {
    +async scan_interface(interface_name) WifiInterfaceScanResult
    +async scan_all_interfaces() List~WifiInterfaceScanResult~
    +async get_status(interface_name) WifiInterfaceStatus
    +async get_all_status() List~WifiInterfaceStatus~
    +async connect(request) dict
    +async disconnect(request) dict
    +async get_saved_networks() List~SavedWifiNetwork~
    +async remove_saved_network(ssid) dict
  }

  class InterfacesRouterV2 {
    +async list_interfaces() WifiInterfaceList
    +async get_interface(interface_name) WifiInterface
  }

  WifiRouterV2 --> AbstractWifiManager
  InterfacesRouterV2 --> AbstractWifiManager

  WifiRouterV2 --> WifiInterfaceStatus
  WifiRouterV2 --> WifiInterfaceScanResult
  WifiRouterV2 --> ConnectRequest
  WifiRouterV2 --> DisconnectRequest
  WifiRouterV2 --> SavedWifiNetwork

  InterfacesRouterV2 --> WifiInterface
  InterfacesRouterV2 --> WifiInterfaceList
Loading

Class diagram for frontend WiFi store and multi-interface types

classDiagram
  class Network {
    +str ssid
    +int signal
    +bool locked
    +bool saved
    +str bssid
    +int frequency
  }

  class WifiInterface_ts {
    +str name
    +bool connected
    +str ssid
    +int signal_strength
    +str ip_address
    +str mac_address
  }

  class WifiInterfaceStatus_ts {
    +str interface
    +str state
    +str ssid
    +str bssid
    +str ip_address
    +int signal_strength
    +int frequency
    +str key_mgmt
  }

  class WifiInterfaceScanResult_ts {
    +str interface
    +WPANetwork[] networks
  }

  class WifiInterfaceList_ts {
    +WifiInterface_ts[] interfaces
    +str hotspot_interface
  }

  class WifiStore {
    +str API_URL
    +str API_URL_V2
    +Network current_network
    +Network[] available_networks
    +Network[] saved_networks
    +bool is_loading
    +WifiInterface_ts[] wifi_interfaces
    +Map~str,Network[]~ interface_scan_results
    +Map~str,WifiInterfaceStatus_ts~ interface_status
    +setCurrentNetwork(network)
    +setAvailableNetworks(networks)
    +setSavedNetworks(networks)
    +setLoading(loading)
    +setWifiInterfaces(interfaces)
    +setInterfaceScanResults(payload)
    +setInterfaceStatus(payload)
    +connectable_networks() Network[]
    +getInterface(name) WifiInterface_ts
    +getInterfaceNetworks(name) Network[]
    +getInterfaceConnectionStatus(name) WifiInterfaceStatus_ts
  }

  WifiStore --> WifiInterface_ts
  WifiStore --> WifiInterfaceStatus_ts
  WifiStore --> Network

  class WifiUpdater {
    +fetch_interfaces_task
    +fetch_interface_scans_task
    +fetch_interface_status_task
    +async fetchInterfaces()
    +async fetchInterfaceScans()
    +async fetchInterfaceStatus()
  }

  WifiUpdater --> WifiStore
  WifiUpdater --> WifiInterfaceList_ts
  WifiUpdater --> WifiInterfaceScanResult_ts
  WifiUpdater --> WifiInterfaceStatus_ts
Loading

Flow diagram for v2 WiFi interface discovery and scanning

flowchart TD
  A_start[Start Wifi service] --> B_start_method[NetworkManagerWifi.start]
  B_start_method --> C_get_devices[_get_wifi_devices]
  C_get_devices --> D_populate_map[Populate _device_paths map keyed by interface name]
  D_populate_map --> E_select_primary[Select primary _device_path
  prefer wlan0 else first]
  E_select_primary --> F_create_virtual[_create_virtual_interface using HOTSPOT_INTERFACE]
  F_create_virtual --> G_autoscan_loop[_autoscan over all _device_paths]

  subgraph Scan_all_interfaces_v2
    G1_call[WifiUpdater.fetchInterfaceScans] --> G2_http[GET /wifi-manager/v2.0/wifi/scan]
    G2_http --> G3_router[wifi_router_v2.scan_all_interfaces]
    G3_router --> G4_manager[NetworkManagerWifi.scan_all_interfaces]
    G4_manager --> G5_scan_device[_scan_device per device_path]
    G5_scan_device --> G6_response[List WifiInterfaceScanResult]
    G6_response --> G7_store[WifiStore.setInterfaceScanResults per interface]
  end

  G_autoscan_loop --> G1_call
Loading

File-Level Changes

Change Details Files
Extend NetworkManager WiFi backend to manage multiple physical interfaces while keeping a primary interface for backward compatibility and hotspot handling.
  • Add constants for hotspot and virtual AP interfaces and track all WiFi device paths keyed by interface name
  • Introduce helper to enumerate WiFi devices excluding the virtual AP interface and prefer wlan0 when selecting the primary device
  • Modify virtual interface creation to always use a designated hotspot device, with fallbacks and improved logging
  • Refactor autoscan to iterate over all discovered WiFi interfaces with per-interface error handling
core/services/wifi/wifi_handlers/networkmanager/networkmanager.py
Add multi-interface operations to the WiFi backend, including interface discovery, per-interface status, scanning, connect, and disconnect, while wrapping them with v1-compatible methods.
  • Introduce WifiInterface-related accessors (list, per-interface status, all-status) that aggregate state from NetworkManager device and IP configuration objects
  • Refactor scan logic into a reusable _scan_device and add per-interface and all-interface scan entrypoints
  • Implement per-interface connect/disconnect methods that bind NetworkManager activations to specific devices and relax connection profiles to be interface-agnostic
  • Keep legacy get_wifi_available, try_connect_to_network, status, and disconnect methods by delegating to the new per-interface APIs based on the primary device
core/services/wifi/wifi_handlers/networkmanager/networkmanager.py
core/services/wifi/typedefs.py
Introduce a versioned WiFi Manager API v2 that exposes multi-interface endpoints while preserving v1 routes.
  • Wire v2 routers into the FastAPI app alongside existing v1 handlers and keep static mounting behavior unchanged
  • Add v2 wifi router exposing per-interface and all-interface scan, status, connect, disconnect, and saved-network operations with graceful degradation when the backend lacks multi-interface support
  • Add v2 interfaces router exposing interface enumeration and per-interface info, falling back to a single wlan0 view when necessary
  • Add a v2 index router that redirects to generated API docs and package the v1/v2 API modules with init files
core/services/wifi/main.py
core/services/wifi/api/v2/routers/wifi.py
core/services/wifi/api/v2/routers/interfaces.py
core/services/wifi/api/v2/routers/index.py
core/services/wifi/api/v2/routers/__init__.py
core/services/wifi/api/__init__.py
core/services/wifi/api/v1/__init__.py
core/services/wifi/api/v2/__init__.py
Update frontend WiFi types, store, and polling logic to consume the v2 multi-interface API while remaining compatible when v2 is unavailable.
  • Extend TypeScript wifi models with WifiInterface, WifiInterfaceList, WifiInterfaceStatus, WifiInterfaceScanResult, and connect/disconnect request types
  • Augment Vuex wifi store with v2 API URL, interface catalog, per-interface scan results and status maps, plus mutations and getters to query them
  • Update WifiUpdater to periodically poll v2 interfaces, interface scans, and interface status endpoints with offline-tolerant error handling
  • Enhance wifi_strenght_icon to handle both percentage and dBm signal formats
core/frontend/src/types/wifi.ts
core/frontend/src/store/wifi.ts
core/frontend/src/components/wifi/WifiUpdater.vue
core/frontend/src/utils/wifi.ts
Redesign frontend WiFi UI to show per-interface tray icons and managers, plus improved network cards with band indicators and dynamic widgets in the main view.
  • Split tray UI so WifiTrayMenu renders one WifiInterfaceTrayMenu per detected interface or falls back to the legacy single-icon menu
  • Add WifiInterfaceTrayMenu and WifiInterfaceManager components that display per-interface status, networks, and hotspot controls, and use InterfaceConnectionDialog for connecting/forgetting
  • Introduce InterfaceConnectionDialog that targets a specific interface via v2 connect/saved endpoints and shows progress and errors
  • Update MainView app list construction to create networking widgets per detected WiFi interface, with a wlan0 fallback, and tweak WifiNetworkCard to show colored 2.4G/5G/6G frequency chips
core/frontend/src/components/wifi/WifiTrayMenu.vue
core/frontend/src/components/wifi/WifiInterfaceTrayMenu.vue
core/frontend/src/components/wifi/WifiInterfaceManager.vue
core/frontend/src/components/wifi/InterfaceConnectionDialog.vue
core/frontend/src/views/MainView.vue
core/frontend/src/components/wifi/WifiNetworkCard.vue
Improve wpa_supplicant backend DHCP handling to respect dynamic interfaces instead of hardcoding wlan0.
  • Change trigger_dhcp_client to derive the interface from the configured socket_name argument when available and fall back to wlan0 for compatibility
core/services/wifi/wifi_handlers/wpa_supplicant/WifiManager.py

Possibly linked issues

  • #(not specified): The PR implements full multi-interface Wi‑Fi support, exactly addressing the request for external/extra Wi‑Fi modules.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In the NetworkManagerWifi methods where you build ip_address from IPv4Config.address_data, you're indexing address_data[0]["address"][1]; this assumes "address" is an indexable sequence – double‑check the actual structure (often it's already a string) and adjust to avoid potential TypeError or incorrect IP extraction.
  • The logic to build WifiInterface/WifiInterfaceStatus objects (state, ssid, signal, IP, MAC) is duplicated across get_wifi_interfaces, get_interface_status, and get_interface_connection_status; consider refactoring this into a shared helper to keep the behavior consistent and make future changes less error‑prone.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the NetworkManagerWifi methods where you build `ip_address` from `IPv4Config.address_data`, you're indexing `address_data[0]["address"][1]`; this assumes `"address"` is an indexable sequence – double‑check the actual structure (often it's already a string) and adjust to avoid potential `TypeError` or incorrect IP extraction.
- The logic to build `WifiInterface`/`WifiInterfaceStatus` objects (state, ssid, signal, IP, MAC) is duplicated across `get_wifi_interfaces`, `get_interface_status`, and `get_interface_connection_status`; consider refactoring this into a shared helper to keep the behavior consistent and make future changes less error‑prone.

## Individual Comments

### Comment 1
<location> `core/services/wifi/wifi_handlers/networkmanager/networkmanager.py:394-402` </location>
<code_context>
+            detail=f"Interface '{interface_name}' not found. Multi-interface support requires NetworkManager.",
+        )
+    status_data = await manager.status()
+    return WifiInterfaceStatus(
+        interface="wlan0",
+        state=status_data.state or "unknown",
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Avoid hardcoding key management to WPA-PSK in interface status

`key_mgmt` is always reported as "WPA-PSK", even for open or enterprise networks. This misrepresents the actual security configuration to v2 API consumers. Where possible, derive `key_mgmt` from the active connection/AP; otherwise leave it unset or expose a more accurate representation than a hardcoded string.
</issue_to_address>

### Comment 2
<location> `core/frontend/src/components/wifi/WifiTrayMenu.vue:93-94` </location>
<code_context>
+    wifi_interfaces(): WifiInterface[] {
+      return wifi.wifi_interfaces
+    },
+    has_multiple_interfaces(): boolean {
+      return this.wifi_interfaces.length > 0
+    },
     wifi_icon(): string {
</code_context>

<issue_to_address>
**nitpick:** Computed name `has_multiple_interfaces` does not match its current behavior

This returns `true` whenever there is at least one interface, so the multi-interface tray UI is used even for a single interface. Either rename the computed to reflect “v2 UI available” (or similar), or change the condition to `this.wifi_interfaces.length > 1` if it should only be `true` when there are actually multiple interfaces. Aligning the name and condition will make the behavior clearer.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +394 to +402
return WifiInterfaceStatus(
interface=interface_name,
state="connected",
ssid=ssid_bytes.decode("utf-8"),
bssid=await ap.hw_address,
ip_address=ip_address,
signal_strength=await ap.strength,
frequency=await ap.frequency,
key_mgmt="WPA-PSK",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Avoid hardcoding key management to WPA-PSK in interface status

key_mgmt is always reported as "WPA-PSK", even for open or enterprise networks. This misrepresents the actual security configuration to v2 API consumers. Where possible, derive key_mgmt from the active connection/AP; otherwise leave it unset or expose a more accurate representation than a hardcoded string.

Comment on lines +93 to +94
has_multiple_interfaces(): boolean {
return this.wifi_interfaces.length > 0
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Computed name has_multiple_interfaces does not match its current behavior

This returns true whenever there is at least one interface, so the multi-interface tray UI is used even for a single interface. Either rename the computed to reflect “v2 UI available” (or similar), or change the condition to this.wifi_interfaces.length > 1 if it should only be true when there are actually multiple interfaces. Aligning the name and condition will make the behavior clearer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant