mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Merge branch 'lrnselfreliance:master' into master
This commit is contained in:
commit
3390c8f464
10 changed files with 208 additions and 70 deletions
|
|
@ -487,3 +487,54 @@ export async function setHotspot(on) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getThrottleStatus() {
|
||||
let response = await apiGet(`${API_URI}/throttle`);
|
||||
if (response.status === 200) {
|
||||
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 throttle status",
|
||||
description: 'Failed to get CPU Throttle status. Check client and server logs.',
|
||||
time: 5000,
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function setThrottle(on) {
|
||||
let response;
|
||||
if (on) {
|
||||
response = await apiPost(`${API_URI}/throttle/on`);
|
||||
} else {
|
||||
response = await apiPost(`${API_URI}/throttle/off`);
|
||||
}
|
||||
if (response.status === 204) {
|
||||
return null;
|
||||
} else {
|
||||
let code = (await response.json())['code'];
|
||||
if (code === 34) {
|
||||
toast({
|
||||
type: 'error',
|
||||
title: 'Unsupported!',
|
||||
description: 'Throttle is only supported on a Raspberry Pi!',
|
||||
time: 5000,
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
type: 'error',
|
||||
title: 'Error!',
|
||||
description: 'Could not modify throttle. See server logs.',
|
||||
time: 5000,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
secondsToFrequency,
|
||||
TabLinks,
|
||||
textEllipsis,
|
||||
ThrottleToggle,
|
||||
WROLModeMessage
|
||||
} from "./Common";
|
||||
import {Route} from "react-router-dom";
|
||||
|
|
@ -80,6 +81,8 @@ class Settings extends React.Component {
|
|||
<WROLModeMessage content='Settings are disabled because WROL Mode is enabled.'/>
|
||||
|
||||
<HotspotToggle/>
|
||||
<br/>
|
||||
<ThrottleToggle style={{marginTop: '0.5em'}}/>
|
||||
|
||||
<Divider/>
|
||||
<p>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import Button from "semantic-ui-react/dist/commonjs/elements/Button";
|
|||
import Grid from "semantic-ui-react/dist/commonjs/collections/Grid";
|
||||
import Message from "semantic-ui-react/dist/commonjs/collections/Message";
|
||||
import {getConfig} from "../api";
|
||||
import {useDownloaders, useHotspot} from "../hooks/customHooks";
|
||||
import {useDownloaders, useHotspot, useThrottle} from "../hooks/customHooks";
|
||||
|
||||
export const API_URI = `http://${window.location.host}/api`;
|
||||
export const VIDEOS_API = `${API_URI}/videos`;
|
||||
|
|
@ -645,9 +645,9 @@ export function CardGroupCentered(props) {
|
|||
)
|
||||
}
|
||||
|
||||
export function HotspotToggle() {
|
||||
export function HotspotToggle(props) {
|
||||
let {on, setHotspot} = useHotspot();
|
||||
return <Checkbox toggle
|
||||
return <Checkbox toggle {...props}
|
||||
label='WiFi Hotspot'
|
||||
disabled={on === null}
|
||||
checked={on === true}
|
||||
|
|
@ -655,6 +655,16 @@ export function HotspotToggle() {
|
|||
/>;
|
||||
}
|
||||
|
||||
export function ThrottleToggle(props) {
|
||||
let {on, setThrottle} = useThrottle();
|
||||
return <Checkbox toggle {...props}
|
||||
label='CPU Power-save'
|
||||
disabled={on === null}
|
||||
checked={on === true}
|
||||
onChange={(e, data) => setThrottle(data.checked)}
|
||||
/>;
|
||||
}
|
||||
|
||||
export function DisableDownloadsToggle() {
|
||||
let {on, setDownloads} = useDownloaders();
|
||||
return <Checkbox toggle
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import {
|
|||
getDownloaders,
|
||||
getFiles,
|
||||
getHotspotStatus,
|
||||
getThrottleStatus,
|
||||
getVersion,
|
||||
killDownloads,
|
||||
searchArchives,
|
||||
searchVideos,
|
||||
setHotspot,
|
||||
setThrottle,
|
||||
startDownloads
|
||||
} from "../api";
|
||||
import {useHistory} from "react-router-dom";
|
||||
|
|
@ -247,3 +249,30 @@ export const useDownloaders = () => {
|
|||
|
||||
return {on, setOn, downloaders, setDownloads: localSetDownloads};
|
||||
}
|
||||
|
||||
export const useThrottle = () => {
|
||||
const [on, setOn] = useState(null);
|
||||
|
||||
const fetchThrottleStatus = async () => {
|
||||
const status = await getThrottleStatus();
|
||||
if (status === 'powersave') {
|
||||
setOn(true);
|
||||
} else if (status === 'ondemand') {
|
||||
setOn(false);
|
||||
} else {
|
||||
setOn(null);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchThrottleStatus();
|
||||
}, []);
|
||||
|
||||
const localSetThrottle = async (on) => {
|
||||
setOn(null);
|
||||
await setThrottle(on);
|
||||
await fetchThrottleStatus();
|
||||
}
|
||||
|
||||
return {on, setOn, setThrottle: localSetThrottle};
|
||||
}
|
||||
|
|
|
|||
13
install.sh
13
install.sh
|
|
@ -84,6 +84,14 @@ cp /opt/wrolpi/50x.html /var/www/50x.html
|
|||
grep wrolpi /etc/passwd || useradd -md /home/wrolpi wrolpi -s "$(command -v bash)"
|
||||
chown -R wrolpi:wrolpi /opt/wrolpi
|
||||
|
||||
# Give WROLPi group a few privileged commands via sudo without password.
|
||||
cat > /etc/sudoers.d/90-wrolpi << 'EOF'
|
||||
%wrolpi ALL=(ALL) NOPASSWD:/usr/bin/nmcli,/usr/bin/cpufreq-set
|
||||
EOF
|
||||
chmod 660 /etc/sudoers.d/90-wrolpi
|
||||
# Verify this new file is valid.
|
||||
visudo -c -f /etc/sudoers.d/90-wrolpi
|
||||
|
||||
# Create the media directory. This should be mounted by the maintainer.
|
||||
[ -d /media/wrolpi ] || mkdir /media/wrolpi
|
||||
chown -R wrolpi:wrolpi /media/wrolpi
|
||||
|
|
@ -106,12 +114,9 @@ sudo -u postgres psql -c '\l' | grep wrolpi || (
|
|||
sudo -u postgres psql -c "alter user wrolpi password 'wrolpi'" &&
|
||||
sudo -u postgres createdb -O wrolpi wrolpi
|
||||
)
|
||||
# Init/upgrade the WROLPi database.
|
||||
# Initialize/upgrade the WROLPi database.
|
||||
(cd /opt/wrolpi && /usr/bin/python3 /opt/wrolpi/main.py db upgrade)
|
||||
|
||||
# Configure the hotspot.
|
||||
bash /opt/wrolpi/scripts/hotspot.sh
|
||||
|
||||
set +x
|
||||
|
||||
ip=$(hostname -i | cut -d' ' -f1)
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
#! /usr/bin/env bash
|
||||
# Configure the WROLPi hotspot.
|
||||
set -x
|
||||
set -e
|
||||
|
||||
apt install -y network-manager
|
||||
|
||||
cat >/etc/netplan/50-cloud-init.yaml <<'EOF'
|
||||
# This file is generated from information provided by the datasource. Changes
|
||||
# to it will not persist across an instance reboot. To disable cloud-init's
|
||||
# network configuration capabilities, write a file
|
||||
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
|
||||
# network: {config: disabled}
|
||||
network:
|
||||
version: 2
|
||||
renderer: NetworkManager
|
||||
EOF
|
||||
|
||||
cat >/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg <<'EOF'
|
||||
network: {config: disabled}
|
||||
EOF
|
||||
|
||||
netplan generate
|
||||
netplan apply
|
||||
|
||||
# Initialize the Hotspot config, then overwrite it.
|
||||
nmcli con add type wifi ifname wlan0 con-name Hotspot autoconnect yes ssid WROLPi
|
||||
cat >/etc/NetworkManager/system-connections/Hotspot.nmconnection <<'EOF'
|
||||
[connection]
|
||||
id=Hotspot
|
||||
uuid=b72ae32e-877c-4559-aaa1-e41a7a9d7bd5
|
||||
type=wifi
|
||||
interface-name=wlan0
|
||||
permissions=
|
||||
timestamp=1635780305
|
||||
|
||||
[wifi]
|
||||
band=bg
|
||||
mac-address-blacklist=
|
||||
mode=ap
|
||||
seen-bssids=DC:A6:32:C7:01:EE;
|
||||
ssid=WROLPi
|
||||
|
||||
[wifi-security]
|
||||
key-mgmt=wpa-psk
|
||||
psk=wrolpi hotspot
|
||||
|
||||
[ipv4]
|
||||
address1=192.168.0.1/24,192.168.0.1
|
||||
dns-search=
|
||||
method=shared
|
||||
|
||||
[ipv6]
|
||||
addr-gen-mode=stable-privacy
|
||||
dns-search=
|
||||
method=auto
|
||||
|
||||
[proxy]
|
||||
EOF
|
||||
|
||||
systemctl restart NetworkManager.service
|
||||
|
|
@ -3,7 +3,7 @@ import pathlib
|
|||
import subprocess
|
||||
|
||||
from wrolpi.common import logger
|
||||
from wrolpi.vars import PYTEST
|
||||
from wrolpi.vars import PYTEST, DEFAULT_CPU_FREQUENCY
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
|
@ -11,10 +11,21 @@ logger = logger.getChild(__name__)
|
|||
SUDO_BIN = pathlib.Path('/usr/bin/sudo')
|
||||
if not SUDO_BIN.is_file() and not PYTEST:
|
||||
logger.error('COULD NOT FIND sudo!!!')
|
||||
|
||||
NMCLI_BIN = pathlib.Path('/usr/bin/nmcli')
|
||||
if not NMCLI_BIN.is_file() and not PYTEST:
|
||||
logger.error('COULD NOT FIND nmcli!!!')
|
||||
|
||||
CPUFREQ_INFO_BIN = pathlib.Path('/usr/bin/cpufreq-info')
|
||||
if not CPUFREQ_INFO_BIN.is_file() and not PYTEST:
|
||||
logger.error('COULD NOT FIND cpufreq-info!!!')
|
||||
|
||||
CPUFREQ_SET_BIN = pathlib.Path('/usr/bin/cpufreq-set')
|
||||
if not CPUFREQ_SET_BIN.is_file() and not PYTEST:
|
||||
logger.error('COULD NOT FIND cpufreq-set!!!')
|
||||
|
||||
POWER_SAVE_FREQ = 'powersave' # noqa
|
||||
|
||||
|
||||
class HotspotStatus(enum.Enum):
|
||||
disconnected = enum.auto() # Radio is on, but Hotspot is not connected.
|
||||
|
|
@ -65,3 +76,45 @@ def disable_hotspot():
|
|||
cmd = (SUDO_BIN, NMCLI_BIN, 'radio', 'wifi', 'off')
|
||||
subprocess.check_call(cmd)
|
||||
return True
|
||||
|
||||
|
||||
class GovernorStatus(enum.Enum):
|
||||
ondemand = enum.auto()
|
||||
powersave = enum.auto()
|
||||
unknown = enum.auto()
|
||||
# These are unused by WROLPi.
|
||||
# performance = enum.auto()
|
||||
# schedutil = enum.auto()
|
||||
# userspace = enum.auto()
|
||||
# conservative = enum.auto()
|
||||
|
||||
|
||||
GOVERNOR_MAP = {
|
||||
'The governor "ondemand" may decide ': GovernorStatus.ondemand,
|
||||
'The governor "powersave" may decide ': GovernorStatus.powersave,
|
||||
}
|
||||
|
||||
|
||||
def throttle_status() -> GovernorStatus:
|
||||
cmd = (CPUFREQ_INFO_BIN,)
|
||||
output = subprocess.check_output(cmd)
|
||||
output = output.decode().strip()
|
||||
|
||||
for line in output.splitlines():
|
||||
for governor, status in GOVERNOR_MAP.items():
|
||||
if governor in line:
|
||||
return status
|
||||
|
||||
return GovernorStatus.unknown
|
||||
|
||||
|
||||
def throttle_cpu_on() -> bool:
|
||||
cmd = (SUDO_BIN, CPUFREQ_SET_BIN, '-g', POWER_SAVE_FREQ)
|
||||
subprocess.check_call(cmd)
|
||||
return True
|
||||
|
||||
|
||||
def throttle_cpu_off() -> bool:
|
||||
cmd = (SUDO_BIN, CPUFREQ_SET_BIN, '-g', DEFAULT_CPU_FREQUENCY)
|
||||
subprocess.check_call(cmd)
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ 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
|
||||
|
|
@ -244,6 +243,38 @@ async def hotspot_off(_: Request):
|
|||
return response.empty(HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
@root_api.get('/throttle')
|
||||
@openapi.description('Get status of CPU throttling')
|
||||
@native_only
|
||||
async def throttle_status(_: Request):
|
||||
status = admin.throttle_status()
|
||||
# Get the name of the enum.
|
||||
status = status.name
|
||||
|
||||
ret = dict(status=status)
|
||||
return json_response(ret)
|
||||
|
||||
|
||||
@root_api.post('/throttle/on')
|
||||
@openapi.description('Turn on CPU throttling')
|
||||
@native_only
|
||||
async def throttle_on(_: Request):
|
||||
result = admin.throttle_cpu_on()
|
||||
if result:
|
||||
return response.empty()
|
||||
return response.empty(HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
@root_api.post('/throttle/off')
|
||||
@openapi.description('Turn off CPU throttling')
|
||||
@native_only
|
||||
async def throttle_off(_: Request):
|
||||
result = admin.throttle_cpu_off()
|
||||
if result:
|
||||
return response.empty()
|
||||
return response.empty(HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
class CustomJSONEncoder(json.JSONEncoder):
|
||||
|
||||
def default(self, obj):
|
||||
|
|
|
|||
|
|
@ -61,3 +61,19 @@ def test_enable_hotspot_connected():
|
|||
call((PosixPath('/usr/bin/sudo'), PosixPath('/usr/bin/nmcli'), 'device', 'wifi', 'hotspot', 'ifname',
|
||||
'wlan0', 'ssid', 'WROLPi', 'password', 'wrolpi hotspot'))]
|
||||
)
|
||||
|
||||
|
||||
def test_throttle_on():
|
||||
with mock.patch('wrolpi.admin.subprocess') as mock_subprocess:
|
||||
assert admin.throttle_cpu_on()
|
||||
mock_subprocess.check_call.assert_called_once_with(
|
||||
(PosixPath('/usr/bin/sudo'), PosixPath('/usr/bin/cpufreq-set'), '-g', 'powersave')
|
||||
)
|
||||
|
||||
|
||||
def test_throttle_off():
|
||||
with mock.patch('wrolpi.admin.subprocess') as mock_subprocess:
|
||||
assert admin.throttle_cpu_off()
|
||||
mock_subprocess.check_call.assert_called_once_with(
|
||||
(PosixPath('/usr/bin/sudo'), PosixPath('/usr/bin/cpufreq-set'), '-g', 'ondemand')
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ LAST_MODIFIED_DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT'
|
|||
DATE_FORMAT = '%Y-%M-%d'
|
||||
DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S'
|
||||
DATETIME_FORMAT_MS = '%Y-%m-%d %H:%M:%S.%f'
|
||||
DEFAULT_CPU_FREQUENCY = 'ondemand'
|
||||
|
||||
DEFAULT_FILE_PERMISSIONS = 0o644
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue