Skip to content

Commit 07e7396

Browse files
wip
Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
1 parent c4b5d08 commit 07e7396

File tree

9 files changed

+477
-92
lines changed

9 files changed

+477
-92
lines changed

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

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,20 @@
1212
</v-toolbar-title>
1313
<v-spacer />
1414
<v-btn
15-
v-if="is_hotspot_interface"
15+
v-if="is_hotspot_running_on_this_interface"
1616
v-tooltip="'Hotspot settings'"
1717
icon
1818
@click="show_settings_dialog = true"
1919
>
2020
<v-icon>mdi-cog</v-icon>
2121
</v-btn>
2222
<v-btn
23-
v-if="is_hotspot_interface"
24-
v-tooltip="'Toggle hotspot'"
23+
v-tooltip="hotspot_status ? 'Disable hotspot' : 'Enable hotspot'"
2524
icon
2625
:color="hotspot_status ? 'success' : 'gray'"
2726
hide-details="auto"
2827
:loading="hotspot_status_loading"
29-
:disabled="hotspot_supported === false"
28+
:disabled="!hotspot_supported"
3029
@click="toggleHotspot"
3130
>
3231
<v-icon>{{ hotspot_status ? 'mdi-access-point' : 'mdi-access-point-off' }}</v-icon>
@@ -94,8 +93,8 @@ import Vue from 'vue'
9493
import Notifier from '@/libs/notifier'
9594
import wifi from '@/store/wifi'
9695
import { wifi_service } from '@/types/frontend_services'
97-
import { Network, WifiInterfaceStatus } from '@/types/wifi'
98-
import back_axios from '@/utils/api'
96+
import { InterfaceHotspotStatus, Network, WifiInterfaceStatus } from '@/types/wifi'
97+
import back_axios, { isBackendOffline } from '@/utils/api'
9998
10099
import SpinningLogo from '../common/SpinningLogo.vue'
101100
import InterfaceConnectionDialog from './InterfaceConnectionDialog.vue'
@@ -125,6 +124,7 @@ export default Vue.extend({
125124
show_settings_dialog: false,
126125
hotspot_status_loading: false,
127126
ssid_filter: undefined as string | undefined,
127+
interface_hotspot_status: null as InterfaceHotspotStatus | null,
128128
}
129129
},
130130
computed: {
@@ -148,16 +148,19 @@ export default Vue.extend({
148148
(network) => network.ssid.toLowerCase().includes(filter),
149149
)
150150
},
151-
is_hotspot_interface(): boolean {
152-
return this.interfaceName === 'wlan0'
151+
is_hotspot_running_on_this_interface(): boolean {
152+
return wifi.current_hotspot_interface === this.interfaceName
153153
},
154-
hotspot_status(): boolean | null {
155-
return wifi.hotspot_status?.enabled ?? null
154+
hotspot_status(): boolean {
155+
return this.interface_hotspot_status?.enabled ?? false
156156
},
157-
hotspot_supported(): boolean | null {
158-
return wifi.hotspot_status?.supported ?? null
157+
hotspot_supported(): boolean {
158+
return this.interface_hotspot_status?.supported ?? true
159159
},
160160
},
161+
mounted() {
162+
this.fetchHotspotStatus()
163+
},
161164
methods: {
162165
isNetworkConnected(network: Network): boolean {
163166
const status = this.connection_status
@@ -180,19 +183,56 @@ export default Vue.extend({
180183
forgetNetwork(network: Network): void {
181184
wifi.forgettNetwork(network)
182185
},
186+
async fetchHotspotStatus(): Promise<void> {
187+
await back_axios({
188+
method: 'get',
189+
url: `${wifi.API_URL_V2}/wifi/hotspot/${this.interfaceName}`,
190+
timeout: 10000,
191+
})
192+
.then((response) => {
193+
this.interface_hotspot_status = response.data
194+
})
195+
.catch((error) => {
196+
if (isBackendOffline(error)) return
197+
// Fallback to v1 API for backward compatibility
198+
this.interface_hotspot_status = {
199+
interface: this.interfaceName,
200+
supported: wifi.hotspot_status?.supported ?? true,
201+
enabled: wifi.hotspot_status?.enabled ?? false,
202+
ssid: null,
203+
password: null,
204+
}
205+
})
206+
},
183207
async toggleHotspot(): Promise<void> {
184208
this.hotspot_status_loading = true
209+
const action = this.hotspot_status ? 'disable' : 'enable'
210+
185211
await back_axios({
186212
method: 'post',
187-
url: `${wifi.API_URL}/hotspot`,
188-
params: { enable: !this.hotspot_status },
189-
timeout: 20000,
213+
url: `${wifi.API_URL_V2}/wifi/hotspot/${action}`,
214+
data: { interface: this.interfaceName },
215+
timeout: 30000,
190216
})
191217
.then(() => {
192-
notifier.pushSuccess('HOTSPOT_STATUS_TOGGLE_SUCCESS', 'Successfully toggled hotspot state.')
218+
notifier.pushSuccess('HOTSPOT_STATUS_TOGGLE_SUCCESS', `Hotspot ${action}d on ${this.interfaceName}.`)
219+
this.fetchHotspotStatus()
193220
})
194221
.catch((error) => {
195-
notifier.pushBackError('HOTSPOT_STATUS_TOGGLE_FAIL', error, true)
222+
if (isBackendOffline(error)) return
223+
// Fallback to v1 API
224+
back_axios({
225+
method: 'post',
226+
url: `${wifi.API_URL}/hotspot`,
227+
params: { enable: !this.hotspot_status },
228+
timeout: 20000,
229+
})
230+
.then(() => {
231+
notifier.pushSuccess('HOTSPOT_STATUS_TOGGLE_SUCCESS', 'Successfully toggled hotspot state.')
232+
})
233+
.catch((fallbackError) => {
234+
notifier.pushBackError('HOTSPOT_STATUS_TOGGLE_FAIL', fallbackError, true)
235+
})
196236
})
197237
.finally(() => {
198238
this.hotspot_status_loading = false

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ export default Vue.extend({
167167
.then((response) => {
168168
const data = response.data as WifiInterfaceList
169169
wifi.setWifiInterfaces(data.interfaces)
170+
wifi.setCurrentHotspotInterface(data.hotspot_interface)
170171
})
171172
.catch((error) => {
172173
// Silently fail - v2 API might not be available

core/frontend/src/store/wifi.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@ import {
44

55
import store from '@/store'
66
import {
7-
Network, NetworkCredentials, SavedNetwork, WifiStatus, HotspotStatus,
8-
WifiInterface, WifiInterfaceList, WifiInterfaceScanResult, WifiInterfaceStatus,
7+
HotspotStatus,
8+
Network,
9+
NetworkCredentials,
10+
SavedNetwork,
11+
WifiInterface,
12+
WifiInterfaceStatus,
13+
WifiStatus,
914
} from '@/types/wifi'
1015
import { sorted_networks } from '@/utils/wifi'
1116

@@ -34,7 +39,7 @@ class WifiStore extends VuexModule {
3439

3540
hotspot_credentials: NetworkCredentials | null = null
3641

37-
is_loading: boolean = true
42+
is_loading = true
3843

3944
// Multi-interface support (v2 API)
4045
wifi_interfaces: WifiInterface[] = []
@@ -43,6 +48,8 @@ class WifiStore extends VuexModule {
4348

4449
interface_status: Map<string, WifiInterfaceStatus> = new Map()
4550

51+
current_hotspot_interface: string | null = null
52+
4653
@Mutation
4754
setCurrentNetwork(network: Network | null): void {
4855
this.current_network = network
@@ -122,6 +129,11 @@ class WifiStore extends VuexModule {
122129
this.interface_status.set(payload.interface_name, payload.status)
123130
}
124131

132+
@Mutation
133+
setCurrentHotspotInterface(interface_name: string | null): void {
134+
this.current_hotspot_interface = interface_name
135+
}
136+
125137
get connectable_networks(): Network[] | null {
126138
if (this.available_networks === null) {
127139
return null

core/frontend/src/types/wifi.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export interface WifiInterface {
6262

6363
export interface WifiInterfaceList {
6464
interfaces: WifiInterface[]
65-
hotspot_interface: string
65+
hotspot_interface: string | null
6666
}
6767

6868
export interface WifiInterfaceStatus {
@@ -90,3 +90,20 @@ export interface ConnectRequest {
9090
export interface DisconnectRequest {
9191
interface: string
9292
}
93+
94+
export interface HotspotRequest {
95+
interface: string
96+
}
97+
98+
export interface HotspotCredentialsRequest {
99+
interface: string
100+
credentials: NetworkCredentials
101+
}
102+
103+
export interface InterfaceHotspotStatus {
104+
interface: string
105+
supported: boolean
106+
enabled: boolean
107+
ssid: string | null
108+
password: string | null
109+
}

core/services/wifi/api/v2/routers/interfaces.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,8 @@ async def list_interfaces() -> WifiInterfaceList:
2929
"""Get list of all available WiFi interfaces on the system.
3030
3131
Returns interface name, connection status, connected SSID (if any),
32-
signal strength, and IP address.
33-
34-
Note: The hotspot interface (wlan0) is included but hotspot operations
35-
are always bound to wlan0 regardless of which interface is used for client connections.
32+
signal strength, and IP address. Also indicates which interface (if any)
33+
is currently running the hotspot.
3634
"""
3735
manager = get_wifi_manager()
3836

@@ -53,10 +51,12 @@ async def list_interfaces() -> WifiInterfaceList:
5351
except Exception as e:
5452
logger.error(f"Error getting interface status: {e}")
5553
interfaces = []
54+
hotspot_interface = "wlan0" if await manager.hotspot_is_running() else None
5655
else:
5756
interfaces = await manager.get_wifi_interfaces()
57+
hotspot_interface = await manager.get_hotspot_interface() if hasattr(manager, "get_hotspot_interface") else None
5858

59-
return WifiInterfaceList(interfaces=interfaces, hotspot_interface="wlan0")
59+
return WifiInterfaceList(interfaces=interfaces, hotspot_interface=hotspot_interface)
6060

6161

6262
@interfaces_router_v2.get("/{interface_name}", response_model=WifiInterface, summary="Get specific interface status.")

core/services/wifi/api/v2/routers/wifi.py

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import sys
2-
from typing import List, cast
2+
from typing import Dict, List, Optional, cast
33

44
from fastapi import APIRouter, HTTPException, status
55
from fastapi_versioning import versioned_api_route
66
from typedefs import (
77
ConnectRequest,
88
DisconnectRequest,
9+
HotspotCredentialsRequest,
10+
HotspotRequest,
11+
InterfaceHotspotStatus,
912
SavedWifiNetwork,
1013
ScannedWifiNetwork,
14+
WifiCredentials,
1115
WifiInterfaceScanResult,
1216
WifiInterfaceStatus,
1317
)
@@ -222,3 +226,120 @@ async def remove_saved_network(ssid: str) -> dict[str, str]:
222226
) from error
223227

224228
return {"status": "removed", "ssid": ssid}
229+
230+
231+
# Hotspot endpoints
232+
233+
234+
@wifi_router_v2.get(
235+
"/hotspot/{interface_name}",
236+
response_model=InterfaceHotspotStatus,
237+
summary="Get hotspot status for a specific interface.",
238+
)
239+
async def get_hotspot_status(interface_name: str) -> InterfaceHotspotStatus:
240+
"""Get hotspot status for a specific WiFi interface."""
241+
manager = get_wifi_manager()
242+
243+
supported = True
244+
enabled = False
245+
credentials: Optional[WifiCredentials] = None
246+
247+
if hasattr(manager, "supports_hotspot_on_interface"):
248+
supported = await manager.supports_hotspot_on_interface(interface_name)
249+
elif hasattr(manager, "supports_hotspot"):
250+
supported = await manager.supports_hotspot()
251+
252+
if hasattr(manager, "hotspot_is_running_on_interface"):
253+
enabled = await manager.hotspot_is_running_on_interface(interface_name)
254+
elif hasattr(manager, "get_hotspot_interface"):
255+
# Check if hotspot is running on this specific interface
256+
current_hotspot_iface = await manager.get_hotspot_interface()
257+
enabled = current_hotspot_iface == interface_name
258+
elif hasattr(manager, "hotspot_is_running"):
259+
# Legacy fallback - can only tell if hotspot is running, not on which interface
260+
# Assume first interface for backward compatibility
261+
enabled = await manager.hotspot_is_running() and interface_name == "wlan0"
262+
263+
if hasattr(manager, "hotspot_credentials"):
264+
credentials = manager.hotspot_credentials()
265+
266+
return InterfaceHotspotStatus(
267+
interface=interface_name,
268+
supported=supported,
269+
enabled=enabled,
270+
ssid=credentials.ssid if credentials else None,
271+
password=credentials.password if credentials else None,
272+
)
273+
274+
275+
@wifi_router_v2.post(
276+
"/hotspot/enable",
277+
summary="Enable hotspot on a specific interface.",
278+
)
279+
async def enable_hotspot(request: HotspotRequest) -> Dict[str, str]:
280+
"""Enable hotspot on a specific WiFi interface.
281+
282+
This allows running a hotspot on any available WiFi interface,
283+
not just the default one.
284+
"""
285+
manager = get_wifi_manager()
286+
287+
try:
288+
if hasattr(manager, "enable_hotspot_on_interface"):
289+
success = await manager.enable_hotspot_on_interface(request.interface)
290+
else:
291+
success = await manager.enable_hotspot()
292+
293+
if not success:
294+
raise HTTPException(
295+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
296+
detail=f"Failed to enable hotspot on {request.interface}",
297+
)
298+
except Exception as e:
299+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) from e
300+
301+
return {"status": "enabled", "interface": request.interface}
302+
303+
304+
@wifi_router_v2.post(
305+
"/hotspot/disable",
306+
summary="Disable hotspot on a specific interface.",
307+
)
308+
async def disable_hotspot(request: HotspotRequest) -> Dict[str, str]:
309+
"""Disable hotspot on a specific WiFi interface."""
310+
manager = get_wifi_manager()
311+
312+
try:
313+
if hasattr(manager, "disable_hotspot_on_interface"):
314+
await manager.disable_hotspot_on_interface(request.interface)
315+
else:
316+
await manager.disable_hotspot()
317+
except Exception as e:
318+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) from e
319+
320+
return {"status": "disabled", "interface": request.interface}
321+
322+
323+
@wifi_router_v2.post(
324+
"/hotspot/credentials",
325+
summary="Update hotspot credentials.",
326+
)
327+
async def set_hotspot_credentials(request: HotspotCredentialsRequest) -> Dict[str, str]:
328+
"""Update the hotspot SSID and password.
329+
330+
Note: Changes take effect on next hotspot enable.
331+
"""
332+
manager = get_wifi_manager()
333+
334+
try:
335+
if hasattr(manager, "set_hotspot_credentials"):
336+
await manager.set_hotspot_credentials(request.credentials)
337+
else:
338+
raise HTTPException(
339+
status_code=status.HTTP_501_NOT_IMPLEMENTED,
340+
detail="Hotspot credential management not supported by this handler",
341+
)
342+
except Exception as e:
343+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) from e
344+
345+
return {"status": "updated", "ssid": request.credentials.ssid}

0 commit comments

Comments
 (0)