diff --git a/app/src/api.js b/app/src/api.js
index 142dc703..b03346e4 100644
--- a/app/src/api.js
+++ b/app/src/api.js
@@ -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,
+ });
+ }
+ }
+}
diff --git a/app/src/components/Admin.js b/app/src/components/Admin.js
index 3ff10020..8efbcc9a 100644
--- a/app/src/components/Admin.js
+++ b/app/src/components/Admin.js
@@ -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 {
+
+
diff --git a/app/src/components/Common.js b/app/src/components/Common.js
index 29ced8b3..54cd25bc 100644
--- a/app/src/components/Common.js
+++ b/app/src/components/Common.js
@@ -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 ;
}
+export function ThrottleToggle(props) {
+ let {on, setThrottle} = useThrottle();
+ return setThrottle(data.checked)}
+ />;
+}
+
export function DisableDownloadsToggle() {
let {on, setDownloads} = useDownloaders();
return {
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};
+}
diff --git a/wrolpi/admin.py b/wrolpi/admin.py
index 4119ae15..14232bbf 100644
--- a/wrolpi/admin.py
+++ b/wrolpi/admin.py
@@ -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
diff --git a/wrolpi/root_api.py b/wrolpi/root_api.py
index 5e30fcc2..48987ff6 100644
--- a/wrolpi/root_api.py
+++ b/wrolpi/root_api.py
@@ -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):
diff --git a/wrolpi/test/test_admin.py b/wrolpi/test/test_admin.py
index b5430ef4..9055e2ae 100644
--- a/wrolpi/test/test_admin.py
+++ b/wrolpi/test/test_admin.py
@@ -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')
+ )
diff --git a/wrolpi/vars.py b/wrolpi/vars.py
index c810884b..4905aba9 100644
--- a/wrolpi/vars.py
+++ b/wrolpi/vars.py
@@ -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