Skip to content

Commit 1ed1308

Browse files
wip
Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
1 parent 9916be5 commit 1ed1308

File tree

9 files changed

+180
-12
lines changed

9 files changed

+180
-12
lines changed

core/frontend/src/components/wifi/WifiSettingsDialog.vue

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<v-dialog
3-
width="300"
3+
width="350"
44
:value="show"
55
@input="showDialog"
66
>
@@ -14,6 +14,17 @@
1414
ref="form"
1515
lazy-validation
1616
>
17+
<v-select
18+
v-model="selected_interface"
19+
:items="available_interfaces"
20+
item-text="name"
21+
item-value="name"
22+
label="Primary WiFi Interface"
23+
hide-details="auto"
24+
:loading="loading_interfaces"
25+
class="mb-4"
26+
/>
27+
1728
<v-text-field
1829
v-model="inputed_ssid"
1930
label="Hotspot SSID"
@@ -64,7 +75,7 @@ import Notifier from '@/libs/notifier'
6475
import wifi from '@/store/wifi'
6576
import { wifi_service } from '@/types/frontend_services'
6677
import { VForm } from '@/types/vuetify'
67-
import { NetworkCredentials } from '@/types/wifi'
78+
import { NetworkCredentials, WifiInterface } from '@/types/wifi'
6879
import back_axios from '@/utils/api'
6980
7081
const notifier = new Notifier(wifi_service)
@@ -87,20 +98,65 @@ export default Vue.extend({
8798
inputed_password: wifi.hotspot_credentials?.password || '',
8899
enable_smart_hotspot: wifi.smart_hotspot_status || false,
89100
saving_settings: false,
101+
available_interfaces: [] as WifiInterface[],
102+
selected_interface: '',
103+
loading_interfaces: false,
90104
}
91105
},
92106
computed: {
93107
form(): VForm {
94108
return this.$refs.form as VForm
95109
},
96110
},
111+
watch: {
112+
show(newVal: boolean): void {
113+
if (newVal) {
114+
this.fetchInterfaces()
115+
}
116+
},
117+
},
97118
methods: {
119+
async fetchInterfaces(): Promise<void> {
120+
this.loading_interfaces = true
121+
try {
122+
const response = await back_axios({
123+
method: 'get',
124+
url: `${wifi.API_URL}/interfaces`,
125+
timeout: 10000,
126+
})
127+
this.available_interfaces = response.data as WifiInterface[]
128+
const primaryInterface = this.available_interfaces.find((iface) => iface.is_primary)
129+
if (primaryInterface) {
130+
this.selected_interface = primaryInterface.name
131+
} else if (this.available_interfaces.length > 0) {
132+
this.selected_interface = this.available_interfaces[0].name
133+
}
134+
} catch (error) {
135+
notifier.pushBackError('WIFI_INTERFACES_FETCH_FAIL', error, true)
136+
} finally {
137+
this.loading_interfaces = false
138+
}
139+
},
98140
async saveSettings(): Promise<boolean> {
99141
if (!this.form.validate()) {
100142
return false
101143
}
102144
this.saving_settings = true
103145
try {
146+
if (this.selected_interface) {
147+
await back_axios({
148+
method: 'post',
149+
url: `${wifi.API_URL}/interfaces/primary`,
150+
params: { interface_name: this.selected_interface },
151+
timeout: 10000,
152+
})
153+
.then(() => {
154+
notifier.pushSuccess('WIFI_INTERFACE_UPDATE_SUCCESS', 'Successfully updated primary WiFi interface.')
155+
})
156+
.catch((error) => {
157+
notifier.pushBackError('WIFI_INTERFACE_UPDATE_FAIL', error, true)
158+
})
159+
}
104160
const credentials: NetworkCredentials = { ssid: this.inputed_ssid, password: this.inputed_password }
105161
await back_axios({
106162
method: 'post',

core/frontend/src/store/wifi.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44

55
import store from '@/store'
66
import {
7-
Network, NetworkCredentials, SavedNetwork, WifiStatus, HotspotStatus
7+
HotspotStatus, Network, NetworkCredentials, SavedNetwork, WifiInterface, WifiStatus,
88
} from '@/types/wifi'
99
import { sorted_networks } from '@/utils/wifi'
1010

@@ -31,6 +31,8 @@ class WifiStore extends VuexModule {
3131

3232
hotspot_credentials: NetworkCredentials | null = null
3333

34+
interfaces: WifiInterface[] | null = null
35+
3436
is_loading: boolean = true
3537

3638
@Mutation
@@ -93,6 +95,11 @@ class WifiStore extends VuexModule {
9395
this.is_loading = loading
9496
}
9597

98+
@Mutation
99+
setInterfaces(interfaces: WifiInterface[] | null): void {
100+
this.interfaces = interfaces
101+
}
102+
96103
get connectable_networks(): Network[] | null {
97104
if (this.available_networks === null) {
98105
return null

core/frontend/src/types/wifi.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,8 @@ export interface NetworkCredentials {
4848
ssid: string
4949
password: string
5050
}
51+
52+
export interface WifiInterface {
53+
name: string
54+
is_primary: boolean
55+
}

core/services/wifi/main.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
SavedWifiNetwork,
2525
ScannedWifiNetwork,
2626
WifiCredentials,
27+
WifiInterface,
2728
)
2829
from uvicorn import Config, Server
2930
from wifi_handlers.AbstractWifiHandler import AbstractWifiManager
@@ -163,6 +164,20 @@ def get_hotspot_credentials() -> Any:
163164
return wifi_manager.hotspot_credentials()
164165

165166

167+
@app.get("/interfaces", response_model=List[WifiInterface], summary="Get available WiFi interfaces.")
168+
@version(1, 0)
169+
async def get_interfaces() -> Any:
170+
assert wifi_manager is not None
171+
return await wifi_manager.get_interfaces()
172+
173+
174+
@app.post("/interfaces/primary", summary="Set the primary WiFi interface.")
175+
@version(1, 0)
176+
async def set_primary_interface(interface_name: str) -> Any:
177+
assert wifi_manager is not None
178+
await wifi_manager.set_primary_interface(interface_name)
179+
180+
166181
app = VersionedFastAPI(app, version="1.0.0", prefix_format="/v{major}.{minor}", enable_latest=True)
167182
app.mount("/", StaticFiles(directory=str(FRONTEND_FOLDER), html=True))
168183

core/services/wifi/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class SettingsV1(settings.BaseSettings):
99
hotspot_enabled = pykson.BooleanField()
1010
hotspot_ssid = pykson.StringField()
1111
hotspot_password = pykson.StringField()
12+
primary_interface = pykson.StringField()
1213
smart_hotspot_enabled = pykson.BooleanField()
1314

1415
def __init__(self, *args: str, **kwargs: int) -> None:

core/services/wifi/typedefs.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,8 @@ class ConnectionStatus(str, Enum):
5959
JUST_CONNECTED = "JUST_CONNECTED"
6060
STILL_CONNECTED = "STILL_CONNECTED"
6161
UNKNOWN = "UNKNOWN"
62+
63+
64+
class WifiInterface(BaseModel):
65+
name: str
66+
is_primary: bool

core/services/wifi/wifi_handlers/AbstractWifiHandler.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from commonwealth.settings.manager import Manager
66
from settings import SettingsV1
7-
from typedefs import SavedWifiNetwork, ScannedWifiNetwork, WifiCredentials, WifiStatus
7+
from typedefs import SavedWifiNetwork, ScannedWifiNetwork, WifiCredentials, WifiInterface, WifiStatus
88
from wifi_handlers.wpa_supplicant.wpa_supplicant import WPASupplicant
99

1010

@@ -109,3 +109,13 @@ def configure(self, _parser: Namespace) -> None:
109109

110110
def add_arguments(self, _parser: ArgumentParser) -> None:
111111
"""Add arguments to the parser"""
112+
113+
@abc.abstractmethod
114+
async def get_interfaces(self) -> List[WifiInterface]:
115+
"""Get available WiFi interfaces."""
116+
raise NotImplementedError
117+
118+
@abc.abstractmethod
119+
async def set_primary_interface(self, interface_name: str) -> None:
120+
"""Set the primary WiFi interface."""
121+
raise NotImplementedError

core/services/wifi/wifi_handlers/networkmanager/networkmanager.py

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
NetworkManagerSettings,
2121
)
2222
from sdbus_async.networkmanager.enums import AccessPointCapabilities, WpaSecurityFlags
23-
from typedefs import SavedWifiNetwork, ScannedWifiNetwork, WifiCredentials, WifiStatus
23+
from typedefs import SavedWifiNetwork, ScannedWifiNetwork, WifiCredentials, WifiInterface, WifiStatus
2424
from wifi_handlers.AbstractWifiHandler import AbstractWifiManager
2525

2626

@@ -107,19 +107,46 @@ async def start(self) -> None:
107107
for sig in (signal.SIGTERM, signal.SIGINT):
108108
loop.add_signal_handler(sig, lambda s=sig: asyncio.create_task(self.handle_shutdown(s)))
109109

110-
# Find WiFi device
110+
# Find WiFi device (use configured interface if set)
111+
await self._select_wifi_device()
112+
113+
# Create virtual AP interface if needed
114+
await self._create_virtual_interface()
115+
self._tasks.append(asyncio.get_event_loop().create_task(self._autoscan()))
116+
self._tasks.append(asyncio.get_event_loop().create_task(self.hotspot_watchdog()))
117+
118+
async def _get_wifi_devices(self) -> List[tuple[str, str]]:
119+
"""Get all WiFi devices as (device_path, interface_name) tuples."""
111120
assert self._nm is not None
121+
wifi_devices: List[tuple[str, str]] = []
112122
devices = await self._nm.get_devices()
113123
for device_path in devices:
114124
device = NetworkDeviceWireless(device_path, self._bus)
115125
if await device.device_type == DeviceType.WIFI:
116-
self._device_path = device_path
117-
break
126+
interface_name = await device.interface
127+
wifi_devices.append((device_path, interface_name))
128+
return wifi_devices
129+
130+
async def _select_wifi_device(self) -> None:
131+
"""Select the WiFi device based on settings or first available."""
132+
wifi_devices = await self._get_wifi_devices()
133+
if not wifi_devices:
134+
logger.warning("No WiFi devices found")
135+
return
118136

119-
# Create virtual AP interface if needed
120-
await self._create_virtual_interface()
121-
self._tasks.append(asyncio.get_event_loop().create_task(self._autoscan()))
122-
self._tasks.append(asyncio.get_event_loop().create_task(self.hotspot_watchdog()))
137+
configured_interface = self._settings_manager.settings.primary_interface
138+
if configured_interface:
139+
for device_path, interface_name in wifi_devices:
140+
if interface_name == configured_interface:
141+
self._device_path = device_path
142+
logger.info(f"Using configured WiFi interface: {configured_interface}")
143+
return
144+
145+
logger.warning(f"Configured interface {configured_interface} not found, using first available")
146+
147+
# Use first available device
148+
self._device_path = wifi_devices[0][0]
149+
logger.info(f"Using WiFi interface: {wifi_devices[0][1]}")
123150

124151
async def _autoscan(self) -> None:
125152

@@ -615,3 +642,31 @@ async def hotspot_watchdog(self) -> None:
615642
if not await self.hotspot_is_running():
616643
logger.info("No network connection detected, enabling hotspot")
617644
await self.enable_hotspot()
645+
646+
async def get_interfaces(self) -> List[WifiInterface]:
647+
"""Get available WiFi interfaces."""
648+
wifi_devices = await self._get_wifi_devices()
649+
current_interface = None
650+
if self._device_path:
651+
device = NetworkDeviceWireless(self._device_path, self._bus)
652+
current_interface = await device.interface
653+
654+
return [
655+
WifiInterface(name=interface_name, is_primary=interface_name == current_interface)
656+
for _, interface_name in wifi_devices
657+
]
658+
659+
async def set_primary_interface(self, interface_name: str) -> None:
660+
"""Set the primary WiFi interface."""
661+
wifi_devices = await self._get_wifi_devices()
662+
available_interfaces = [name for _, name in wifi_devices]
663+
664+
if interface_name not in available_interfaces:
665+
raise ValueError(f"Interface {interface_name} not found. Available: {available_interfaces}")
666+
667+
self._settings_manager.settings.primary_interface = interface_name
668+
self._settings_manager.save()
669+
670+
# Switch to the new interface
671+
await self._select_wifi_device()
672+
logger.info(f"Primary interface set to: {interface_name}")

core/services/wifi/wifi_handlers/wpa_supplicant/WifiManager.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
SavedWifiNetwork,
1818
ScannedWifiNetwork,
1919
WifiCredentials,
20+
WifiInterface,
2021
WifiStatus,
2122
)
2223
from wifi_handlers.AbstractWifiHandler import AbstractWifiManager
@@ -577,3 +578,16 @@ async def supports_hotspot(self) -> bool:
577578

578579
async def hotspot_is_running(self) -> bool:
579580
return self.hotspot.is_running()
581+
582+
async def get_interfaces(self) -> List[WifiInterface]:
583+
"""Get available WiFi interfaces (wpa_supplicant only uses one interface)."""
584+
if self.wpa_path:
585+
interface_name = os.path.basename(self.wpa_path)
586+
return [WifiInterface(name=interface_name, is_primary=True)]
587+
return []
588+
589+
async def set_primary_interface(self, interface_name: str) -> None:
590+
"""Set primary interface - wpa_supplicant requires restart to change interface."""
591+
self._settings_manager.settings.primary_interface = interface_name
592+
self._settings_manager.save()
593+
logger.info(f"Primary interface set to: {interface_name}. Restart required to take effect.")

0 commit comments

Comments
 (0)