mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Hotspot can be toggled in UI. Tested on RPi.
This commit is contained in:
parent
d935da95f2
commit
41e89843bf
5 changed files with 121 additions and 223 deletions
|
|
@ -437,16 +437,23 @@ export async function getVersion() {
|
|||
return data['version'];
|
||||
}
|
||||
|
||||
export async function getDevices() {
|
||||
export async function getHotspotStatus() {
|
||||
let response = await apiGet(`${API_URI}/hotspot`);
|
||||
if (response.status === 200) {
|
||||
let {devices} = await response.json();
|
||||
return devices;
|
||||
let {status} = await response.json();
|
||||
return status;
|
||||
} else {
|
||||
let code = (await response.json())['code'];
|
||||
if (response.status === 400 && code === 34) {
|
||||
// Must not be running on a RPi. Don't warn the user.
|
||||
return null;
|
||||
} else {
|
||||
toast({
|
||||
type: 'error',
|
||||
title: "Can't get Hotspot status",
|
||||
description: 'Failed to get Hotspot status. Check client and server logs.',
|
||||
time: 5000,
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
|
@ -470,6 +477,13 @@ export async function setHotspot(on) {
|
|||
description: 'Hotspot is only supported on a Raspberry Pi!',
|
||||
time: 5000,
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
type: 'error',
|
||||
title: 'Error!',
|
||||
description: 'Could not modify hotspot. See server logs.',
|
||||
time: 5000,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import {useEffect, useState} from "react";
|
|||
import {
|
||||
fetchDomains,
|
||||
filesSearch,
|
||||
getDevices,
|
||||
getHotspotStatus,
|
||||
getDownloaders,
|
||||
getFiles,
|
||||
getVersion,
|
||||
|
|
@ -194,22 +194,20 @@ export const useBrowseFiles = () => {
|
|||
export const useHotspot = () => {
|
||||
const [on, setOn] = useState(null);
|
||||
|
||||
const fetchDevices = async () => {
|
||||
const devices = await getDevices();
|
||||
if (devices && devices['wlan0']) {
|
||||
let wlan0 = devices['wlan0'];
|
||||
setOn(wlan0['connection'] === 'connected');
|
||||
}
|
||||
const fetchHotspotStatus = async () => {
|
||||
const status = await getHotspotStatus();
|
||||
// Wi-Fi can be: connected, disconnected, unavailable.
|
||||
setOn(status === 'connected');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchDevices();
|
||||
});
|
||||
fetchHotspotStatus();
|
||||
}, []);
|
||||
|
||||
const localSetHotspot = async (on) => {
|
||||
setOn(null);
|
||||
await setHotspot(on);
|
||||
await fetchDevices();
|
||||
await fetchHotspotStatus();
|
||||
}
|
||||
|
||||
return {on, setOn, setHotspot: localSetHotspot};
|
||||
|
|
|
|||
101
wrolpi/admin.py
101
wrolpi/admin.py
|
|
@ -1,5 +1,5 @@
|
|||
import enum
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from wrolpi.common import logger
|
||||
|
|
@ -15,76 +15,53 @@ NMCLI_BIN = pathlib.Path('/usr/bin/nmcli')
|
|||
if not NMCLI_BIN.is_file() and not PYTEST:
|
||||
logger.error('COULD NOT FIND nmcli!!!')
|
||||
|
||||
DEVICE_NAMES = ('wlan0', 'eth0')
|
||||
|
||||
DEVICE_MATCH = re.compile(r'^(.+?): (connected|unavailable).*$')
|
||||
MAC_MATCH = re.compile(r'.+?(\w+?) \(.+?\), ([0-9A-F:]{17}).*')
|
||||
INET_MATCH = re.compile(r'.+?(inet[46]) ([a-f0-9\./:]+).*')
|
||||
class HotspotStatus(enum.Enum):
|
||||
disconnected = enum.auto() # Radio is on, but Hotspot is not connected.
|
||||
unavailable = enum.auto() # Radio is off.
|
||||
connected = enum.auto() # Radio is on, Hotspot is on.
|
||||
|
||||
|
||||
def parse_nmcli_status(status: bytes) -> dict:
|
||||
status = status.decode()
|
||||
|
||||
parsed = dict()
|
||||
device = dict()
|
||||
device_name = None
|
||||
for line in status.splitlines():
|
||||
if match := DEVICE_MATCH.match(line):
|
||||
device_name, connection = match.groups()
|
||||
if device_name not in DEVICE_NAMES:
|
||||
# We don't care about this device.
|
||||
device_name = None
|
||||
continue
|
||||
device['connection'] = connection
|
||||
elif match := MAC_MATCH.match(line):
|
||||
kind, mac = match.groups()
|
||||
device['kind'] = kind
|
||||
device['mac'] = mac
|
||||
elif match := INET_MATCH.match(line):
|
||||
net, ip = match.groups()
|
||||
if net == 'inet4':
|
||||
device[net] = ip
|
||||
elif net in device:
|
||||
device[net].append(ip)
|
||||
else:
|
||||
device[net] = [ip, ]
|
||||
elif line == '' and device_name:
|
||||
# Completed this device block
|
||||
parsed[device_name] = device
|
||||
device = dict()
|
||||
device_name = None
|
||||
|
||||
return parsed
|
||||
|
||||
|
||||
def hotspot_status():
|
||||
def hotspot_status() -> HotspotStatus:
|
||||
cmd = (NMCLI_BIN,)
|
||||
output = subprocess.check_output(cmd, stderr=subprocess.PIPE, timeout=10)
|
||||
output = subprocess.check_output(cmd).decode().strip()
|
||||
|
||||
parsed = parse_nmcli_status(output)
|
||||
for line in output.splitlines():
|
||||
if line.startswith('wlan0: connected'):
|
||||
return HotspotStatus.connected
|
||||
elif line.startswith('wlan0: disconnected'):
|
||||
return HotspotStatus.disconnected
|
||||
elif line.startswith('wlan0: unavailable'):
|
||||
return HotspotStatus.unavailable
|
||||
|
||||
devices = dict()
|
||||
for device_name, status in parsed.items():
|
||||
devices[device_name] = dict(
|
||||
kind=status.get('kind'),
|
||||
inet4=status.get('inet4'),
|
||||
inet6=status.get('inet6'),
|
||||
connection=status.get('connection'),
|
||||
)
|
||||
return devices
|
||||
raise Exception(f'Unknown status! {output=}')
|
||||
|
||||
|
||||
def hotspot_on():
|
||||
cmd = (SUDO_BIN, NMCLI_BIN, 'radio', 'wifi', 'on')
|
||||
code = subprocess.check_call(cmd)
|
||||
if code == 0:
|
||||
def enable_hotspot():
|
||||
"""
|
||||
Turn the wlan0 interface into a hotspot. If wlan0 is already running, replace that with a hotspot.
|
||||
"""
|
||||
status = hotspot_status()
|
||||
logger.warning(f'Hotspot status: {status}')
|
||||
|
||||
if status == HotspotStatus.connected:
|
||||
disable_hotspot()
|
||||
return enable_hotspot()
|
||||
elif status == HotspotStatus.disconnected:
|
||||
# Radio is on, but not connected. Good, turn it into a hotspot.
|
||||
cmd = (SUDO_BIN, NMCLI_BIN, 'device', 'wifi', 'hotspot', 'ifname', 'wlan0',
|
||||
'ssid', 'WROLPi', 'password', 'wrolpi hotspot')
|
||||
subprocess.check_call(cmd)
|
||||
return True
|
||||
return False
|
||||
elif status == HotspotStatus.unavailable:
|
||||
# Radio is not on, turn it on.
|
||||
cmd = (SUDO_BIN, NMCLI_BIN, 'radio', 'wifi', 'on')
|
||||
subprocess.check_call(cmd)
|
||||
return enable_hotspot()
|
||||
|
||||
|
||||
def hotspot_off():
|
||||
def disable_hotspot():
|
||||
"""Turn off the wlan0 interface."""
|
||||
cmd = (SUDO_BIN, NMCLI_BIN, 'radio', 'wifi', 'off')
|
||||
code = subprocess.check_call(cmd)
|
||||
if code == 0:
|
||||
return True
|
||||
return False
|
||||
subprocess.check_call(cmd)
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from sanic_ext import validate
|
|||
from sanic_ext.extensions.openapi import openapi
|
||||
|
||||
from wrolpi import admin
|
||||
from wrolpi.admin import HotspotStatus
|
||||
from wrolpi.common import set_sanic_url_parts, logger, get_config, wrol_mode_enabled, save_settings_config, \
|
||||
Base, get_media_directory, wrol_mode_check, native_only
|
||||
from wrolpi.dates import set_timezone
|
||||
|
|
@ -215,8 +216,11 @@ async def get_downloaders(_: Request):
|
|||
@native_only
|
||||
@openapi.description('Get the status of the hotspot')
|
||||
async def hotspot(_: Request):
|
||||
devices = admin.hotspot_status()
|
||||
ret = dict(devices=devices)
|
||||
status = admin.hotspot_status()
|
||||
# Get the name of the enum.
|
||||
status = status.name
|
||||
|
||||
ret = dict(status=status)
|
||||
return json_response(ret)
|
||||
|
||||
|
||||
|
|
@ -224,7 +228,7 @@ async def hotspot(_: Request):
|
|||
@openapi.description('Turn on the hotspot')
|
||||
@native_only
|
||||
async def hotspot_on(_: Request):
|
||||
result = admin.hotspot_off()
|
||||
result = admin.enable_hotspot()
|
||||
if result:
|
||||
return response.empty()
|
||||
return response.empty(HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||
|
|
@ -233,8 +237,8 @@ async def hotspot_on(_: Request):
|
|||
@root_api.post('/hotspot/off')
|
||||
@openapi.description('Turn off the hotspot')
|
||||
@native_only
|
||||
async def hotspot_(_: Request):
|
||||
result = admin.hotspot_off()
|
||||
async def hotspot_off(_: Request):
|
||||
result = admin.disable_hotspot()
|
||||
if result:
|
||||
return response.empty()
|
||||
return response.empty(HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||
|
|
|
|||
|
|
@ -1,158 +1,63 @@
|
|||
from pathlib import PosixPath
|
||||
from unittest import mock
|
||||
from unittest.mock import call
|
||||
|
||||
from wrolpi import admin
|
||||
|
||||
nmcli_status_example1 = b'''wlan0: connected to Hotspot
|
||||
"Broadcom BCM43438 combo and Bluetooth Low Energy"
|
||||
wifi (brcmfmac), AB:CD:EF:F1:23:45, hw, mtu 1500
|
||||
inet4 192.168.0.1/24
|
||||
route4 192.168.0.0/24
|
||||
route4 0.0.0.0/0
|
||||
inet6 fe80::5a1b:7312:ab3:80e7/64
|
||||
route6 fe80::/64
|
||||
|
||||
eth0: connected to Wired connection 1
|
||||
"eth0"
|
||||
ethernet (bcmgenet), AB:CD:EF:F1:23:45, hw, mtu 1500
|
||||
ip4 default
|
||||
inet4 10.0.0.6/24
|
||||
route4 0.0.0.0/0
|
||||
route4 10.0.0.0/24
|
||||
inet6 fd57:d3d7:5699:4a24:b1ec:31b8:1f7a:fa13/64
|
||||
inet6 fe80::611b:154f:8022:c8ca/64
|
||||
route6 fdbd:1f76:f9a5::/64
|
||||
route6 fd57:d3d7:5699:4a24::/64
|
||||
route6 fe80::/64
|
||||
|
||||
p2p-dev-wlan0: disconnected
|
||||
"p2p-dev-wlan0"
|
||||
wifi-p2p, hw
|
||||
|
||||
lo: unmanaged
|
||||
"lo"
|
||||
loopback (unknown), 00:00:00:00:00:00, sw, mtu 65536
|
||||
|
||||
DNS configuration:
|
||||
servers: 10.0.0.1
|
||||
interface: eth0
|
||||
|
||||
Use "nmcli device show" to get complete information about known devices and
|
||||
"nmcli connection show" to get an overview on active connection profiles.
|
||||
|
||||
Consult nmcli(1) and nmcli-examples(7) manual pages for complete usage details.
|
||||
'''
|
||||
|
||||
nmcli_status_example2 = b'''eth0: connected to Wired connection 1
|
||||
"eth0"
|
||||
ethernet (bcmgenet), AB:CD:EF:F1:23:45, hw, mtu 1500
|
||||
ip4 default
|
||||
inet4 10.0.0.6/24
|
||||
route4 0.0.0.0/0
|
||||
route4 10.0.0.0/24
|
||||
inet6 fd57:d3d7:5699:4a24:b1ec:31b8:1f7a:fa13/64
|
||||
inet6 fe80::611b:154f:8022:c8ca/64
|
||||
route6 fdbd:1f76:f9a5::/64
|
||||
route6 fd57:d3d7:5699:4a24::/64
|
||||
route6 fe80::/64
|
||||
|
||||
wlan0: unavailable
|
||||
"Broadcom BCM43438 combo and Bluetooth Low Energy"
|
||||
wifi (brcmfmac), AB:CD:EF:F1:23:45, sw disabled, hw, mtu 1500
|
||||
|
||||
p2p-dev-wlan0: unavailable
|
||||
"p2p-dev-wlan0"
|
||||
wifi-p2p, sw disabled, hw
|
||||
|
||||
lo: unmanaged
|
||||
"lo"
|
||||
loopback (unknown), 00:00:00:00:00:00, sw, mtu 65536
|
||||
|
||||
DNS configuration:
|
||||
servers: 10.0.0.1
|
||||
interface: eth0
|
||||
|
||||
Use "nmcli device show" to get complete information about known devices and
|
||||
"nmcli connection show" to get an overview on active connection profiles.
|
||||
|
||||
Consult nmcli(1) and nmcli-examples(7) manual pages for complete usage details.
|
||||
'''
|
||||
|
||||
|
||||
def test_parse_nmcli_status():
|
||||
result = admin.parse_nmcli_status(nmcli_status_example1)
|
||||
assert result == {
|
||||
'wlan0': {
|
||||
'connection': 'connected',
|
||||
'kind': 'wifi',
|
||||
'mac': 'AB:CD:EF:F1:23:45',
|
||||
'inet4': '192.168.0.1/24',
|
||||
'inet6': ['fe80::5a1b:7312:ab3:80e7/64'],
|
||||
},
|
||||
'eth0': {
|
||||
'connection': 'connected',
|
||||
'kind': 'ethernet',
|
||||
'mac': 'AB:CD:EF:F1:23:45',
|
||||
'inet4': '10.0.0.6/24',
|
||||
'inet6': ['fd57:d3d7:5699:4a24:b1ec:31b8:1f7a:fa13/64', 'fe80::611b:154f:8022:c8ca/64'],
|
||||
},
|
||||
}
|
||||
|
||||
result = admin.parse_nmcli_status(nmcli_status_example2)
|
||||
assert result == {
|
||||
'wlan0': {
|
||||
'connection': 'unavailable',
|
||||
'kind': 'wifi',
|
||||
'mac': 'AB:CD:EF:F1:23:45',
|
||||
},
|
||||
'eth0': {
|
||||
'connection': 'connected',
|
||||
'kind': 'ethernet',
|
||||
'mac': 'AB:CD:EF:F1:23:45',
|
||||
'inet4': '10.0.0.6/24',
|
||||
'inet6': ['fd57:d3d7:5699:4a24:b1ec:31b8:1f7a:fa13/64', 'fe80::611b:154f:8022:c8ca/64'],
|
||||
},
|
||||
}
|
||||
from wrolpi.admin import HotspotStatus
|
||||
|
||||
|
||||
def test_hotspot_status():
|
||||
with mock.patch('wrolpi.admin.subprocess') as mock_subprocess:
|
||||
mock_subprocess.check_output.return_value = nmcli_status_example1
|
||||
assert admin.hotspot_status() == {
|
||||
'wlan0': {
|
||||
'connection': 'connected',
|
||||
'kind': 'wifi',
|
||||
'inet4': '192.168.0.1/24',
|
||||
'inet6': ['fe80::5a1b:7312:ab3:80e7/64'],
|
||||
},
|
||||
'eth0': {
|
||||
'connection': 'connected',
|
||||
'kind': 'ethernet',
|
||||
'inet4': '10.0.0.6/24',
|
||||
'inet6': ['fd57:d3d7:5699:4a24:b1ec:31b8:1f7a:fa13/64', 'fe80::611b:154f:8022:c8ca/64'],
|
||||
}
|
||||
}
|
||||
mock_subprocess.check_output.return_value = b'wlan0: unavailable'
|
||||
assert admin.hotspot_status() == HotspotStatus.unavailable
|
||||
mock_subprocess.check_output.return_value = b'wlan0: disconnected'
|
||||
assert admin.hotspot_status() == HotspotStatus.disconnected
|
||||
mock_subprocess.check_output.return_value = b'wlan0: connected'
|
||||
assert admin.hotspot_status() == HotspotStatus.connected
|
||||
|
||||
|
||||
def test_hotspot_on():
|
||||
with mock.patch('wrolpi.admin.subprocess') as mock_subprocess:
|
||||
mock_subprocess.check_call.return_value = 0
|
||||
assert admin.hotspot_on() is True
|
||||
def test_enable_hotspot_disconnected():
|
||||
"""Enable hotspot with wifi on."""
|
||||
with mock.patch('wrolpi.admin.hotspot_status') as mock_hotspot_status, \
|
||||
mock.patch('wrolpi.admin.subprocess') as mock_subprocess:
|
||||
mock_hotspot_status.return_value = HotspotStatus.disconnected
|
||||
mock_subprocess.check_output.return_value = 0
|
||||
assert admin.enable_hotspot() is True
|
||||
mock_subprocess.check_call.assert_called_once_with(
|
||||
(PosixPath('/usr/bin/sudo'), PosixPath('/usr/bin/nmcli'), 'radio', 'wifi', 'on')
|
||||
(PosixPath('/usr/bin/sudo'), PosixPath('/usr/bin/nmcli'), 'device', 'wifi', 'hotspot', 'ifname', 'wlan0',
|
||||
'ssid', 'WROLPi', 'password', 'wrolpi hotspot'))
|
||||
|
||||
|
||||
def test_enable_hotspot_unavailable():
|
||||
"""Enable hotspot with wifi off."""
|
||||
with mock.patch('wrolpi.admin.hotspot_status') as mock_hotspot_status, \
|
||||
mock.patch('wrolpi.admin.subprocess') as mock_subprocess:
|
||||
mock_hotspot_status.side_effect = [HotspotStatus.unavailable, HotspotStatus.disconnected]
|
||||
mock_subprocess.check_output.return_value = 0
|
||||
assert admin.enable_hotspot() is True
|
||||
mock_subprocess.check_call.assert_has_calls(
|
||||
[
|
||||
call((PosixPath('/usr/bin/sudo'), PosixPath('/usr/bin/nmcli'), 'radio', 'wifi', 'on')),
|
||||
call((PosixPath('/usr/bin/sudo'), PosixPath('/usr/bin/nmcli'), 'device', 'wifi', 'hotspot', 'ifname',
|
||||
'wlan0', 'ssid', 'WROLPi', 'password', 'wrolpi hotspot'))]
|
||||
)
|
||||
|
||||
mock_subprocess.check_call.return_value = 1
|
||||
assert admin.hotspot_on() is False
|
||||
|
||||
|
||||
def test_hotspot_off():
|
||||
with mock.patch('wrolpi.admin.subprocess') as mock_subprocess:
|
||||
mock_subprocess.check_call.return_value = 0
|
||||
assert admin.hotspot_off() is True
|
||||
mock_subprocess.check_call.assert_called_once_with(
|
||||
(PosixPath('/usr/bin/sudo'), PosixPath('/usr/bin/nmcli'), 'radio', 'wifi', 'off')
|
||||
def test_enable_hotspot_connected():
|
||||
"""Enable hotspot with the hotspot alaready enabled."""
|
||||
with mock.patch('wrolpi.admin.hotspot_status') as mock_hotspot_status, \
|
||||
mock.patch('wrolpi.admin.subprocess') as mock_subprocess:
|
||||
mock_hotspot_status.side_effect = [
|
||||
HotspotStatus.connected,
|
||||
HotspotStatus.unavailable,
|
||||
HotspotStatus.disconnected,
|
||||
]
|
||||
mock_subprocess.check_output.return_value = 0
|
||||
assert admin.enable_hotspot() is True
|
||||
mock_subprocess.check_call.assert_has_calls(
|
||||
[
|
||||
call((PosixPath('/usr/bin/sudo'), PosixPath('/usr/bin/nmcli'), 'radio', 'wifi', 'off')),
|
||||
call((PosixPath('/usr/bin/sudo'), PosixPath('/usr/bin/nmcli'), 'radio', 'wifi', 'on')),
|
||||
call((PosixPath('/usr/bin/sudo'), PosixPath('/usr/bin/nmcli'), 'device', 'wifi', 'hotspot', 'ifname',
|
||||
'wlan0', 'ssid', 'WROLPi', 'password', 'wrolpi hotspot'))]
|
||||
)
|
||||
|
||||
mock_subprocess.check_call.return_value = 1
|
||||
assert admin.hotspot_off() is False
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue