Add basic fan control UI and functionality
This commit is contained in:
parent
e3a5524b93
commit
ec8f387f0a
3 changed files with 328 additions and 11 deletions
|
@ -3,6 +3,7 @@
|
|||
Steam Deck fan controls.
|
||||
|
||||
This is generated from the template plugin for the [SteamOS Plugin Loader](https://github.com/SteamDeckHomebrew/PluginLoader).
|
||||
You will need that installed for this plugin to work.
|
||||
|
||||
## License
|
||||
|
||||
|
|
178
main.py
178
main.py
|
@ -1,12 +1,174 @@
|
|||
class Plugin:
|
||||
# A normal method. It can be called from JavaScript using call_plugin_function("method_1", argument1, argument2)
|
||||
async def method_1(self, *args):
|
||||
pass
|
||||
import json
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
# A normal method. It can be called from JavaScript using call_plugin_function("method_2", argument1, argument2)
|
||||
async def method_2(self, *args):
|
||||
pass
|
||||
import logging
|
||||
|
||||
logging.basicConfig(
|
||||
filename = "/home/deck/.fantastic.log",
|
||||
format = '%(asctime)s %(levelname)s %(message)s',
|
||||
filemode = 'w',
|
||||
force = True)
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
FAN_MINIMUM = 0
|
||||
FAN_MAXIMUM = 7000 # max is more around 7100
|
||||
|
||||
TEMPERATURE_MINIMUM = 0.0
|
||||
TEMPERATURE_MAXIMUM = 100.0
|
||||
|
||||
DATA_SAVE_FILE = "fantastic.json"
|
||||
DATA_SAVE_FOLDER = "/home/deck/.config/fantastic/"
|
||||
DATA_SAVE_PATH = DATA_SAVE_FOLDER + DATA_SAVE_FILE
|
||||
|
||||
DEFAULT_DATA = {
|
||||
"version": 0,
|
||||
"enable": False,
|
||||
"interpolate": False,
|
||||
"curve": [],
|
||||
}
|
||||
|
||||
class Plugin:
|
||||
settings = None
|
||||
is_changed = False
|
||||
|
||||
plot_width = 1;
|
||||
plot_height = 1;
|
||||
|
||||
period_s = 1.0;
|
||||
|
||||
async def set_curve(self, curve):
|
||||
await self.wait_for_ready(self)
|
||||
self.settings["curve"] = curve
|
||||
self.is_changed = True
|
||||
|
||||
async def get_curve(self):
|
||||
await self.wait_for_ready(self)
|
||||
return self.settings["curve"]
|
||||
|
||||
async def get_curve_point(self, index):
|
||||
await self.wait_for_ready(self)
|
||||
return self.settings["curve"][index]
|
||||
|
||||
async def set_curve_point(self, index, point):
|
||||
await self.wait_for_ready(self)
|
||||
self.settings["curve"][index] = point
|
||||
self.is_changed = True
|
||||
|
||||
async def add_curve_point(self, point):
|
||||
await self.wait_for_ready(self)
|
||||
self.settings["curve"].append(point)
|
||||
self.settings["curve"].sort(key=lambda p: p["x"])
|
||||
self.is_changed = True
|
||||
x = point["x"]
|
||||
y = point["y"]
|
||||
logger.debug(f"Added point (Temp:{100*x},PWM%:{100*y}) ~= ({x*self.plot_width},{y*self.plot_height})")
|
||||
|
||||
async def remove_curve_point(self, index):
|
||||
await self.wait_for_ready(self)
|
||||
del(self.settings["curve"][index])
|
||||
self.is_changed = True
|
||||
|
||||
async def set_enable(self, enable: bool):
|
||||
await self.wait_for_ready(self)
|
||||
self.settings["enable"] = enable
|
||||
on_set_enable(enable)
|
||||
self.is_changed = True
|
||||
|
||||
async def get_enable(self) -> bool:
|
||||
await self.wait_for_ready(self)
|
||||
return self.settings["enable"]
|
||||
|
||||
async def set_plot_size(self, x, y):
|
||||
logging.debug(f"Set plot size to ({x},{y})")
|
||||
self.plot_width = x
|
||||
self.plot_height = y
|
||||
|
||||
async def set_poll_period(self, period):
|
||||
self.period_s = period
|
||||
|
||||
def save(self):
|
||||
if not os.path.exists(DATA_SAVE_FOLDER):
|
||||
os.mkdir(DATA_SAVE_FOLDER)
|
||||
with open(DATA_SAVE_PATH, "w") as data_file :
|
||||
json.dump(self.settings, data_file)
|
||||
|
||||
async def wait_for_ready(self):
|
||||
while self.settings is None:
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
def do_fan_control(self):
|
||||
index = -1
|
||||
curve = self.settings["curve"]
|
||||
temperature = (thermal_zone(0) - TEMPERATURE_MINIMUM) / TEMPERATURE_MAXIMUM
|
||||
for i in range(len(curve)-1, -1, -1):
|
||||
if curve[i]["x"] < temperature:
|
||||
index = i
|
||||
break
|
||||
if index != -1:
|
||||
target_speed = ((1 - curve[index]["y"]) * FAN_MAXIMUM) + FAN_MINIMUM
|
||||
set_fan_target(int(target_speed))
|
||||
else:
|
||||
if len(curve) == 0:
|
||||
set_fan_target(int(FAN_MAXIMUM))
|
||||
else:
|
||||
set_fan_target(int((FAN_MINIMUM + FAN_MAXIMUM) / 2))
|
||||
|
||||
# Asyncio-compatible long-running code, executed in a task when the plugin is loaded
|
||||
async def _main(self):
|
||||
pass
|
||||
# startup
|
||||
if os.path.exists(DATA_SAVE_PATH):
|
||||
with open(DATA_SAVE_PATH, "r") as data_file:
|
||||
self.settings = json.load(data_file)
|
||||
else:
|
||||
self.settings = dict(DEFAULT_DATA)
|
||||
try:
|
||||
self.settings["version"]
|
||||
except:
|
||||
self.settings = dict(DEFAULT_DATA)
|
||||
while self.settings["version"] != DEFAULT_DATA["version"]:
|
||||
# TODO specific upgrade functionality
|
||||
self.settings["version"] = DEFAULT_DATA["version"]
|
||||
self.settings["enable"] = DEFAULT_DATA["enable"]
|
||||
self.settings["interpolate"] = DEFAULT_DATA["interpolate"]
|
||||
self.settings["curve"] = DEFAULT_DATA["curve"]
|
||||
self.is_changed = True
|
||||
on_set_enable(self.settings["enable"])
|
||||
# work loop
|
||||
while True:
|
||||
if self.is_changed:
|
||||
self.save(self)
|
||||
self.is_changed = False
|
||||
if self.settings["enable"]:
|
||||
# custom fan curve is enabled
|
||||
self.do_fan_control(self)
|
||||
await asyncio.sleep(self.period_s)
|
||||
|
||||
def thermal_zone(index: int):
|
||||
with open(f"/sys/class/thermal/thermal_zone{index}/temp", "r") as f:
|
||||
result = float(f.read().strip()) / 1000.0
|
||||
logging.debug(f"Got {result}'C from thermal_zone{index}")
|
||||
return result
|
||||
|
||||
def set_fan_target(rpm: int):
|
||||
logging.debug(f"Setting fan1_target to {rpm}")
|
||||
with open("/sys/class/hwmon/hwmon5/fan1_target", "w") as f:
|
||||
f.write(str(rpm))
|
||||
|
||||
def on_enable():
|
||||
with open("/sys/class/hwmon/hwmon5/recalculate", "w") as f:
|
||||
f.write("1")
|
||||
# TODO disable system fan control
|
||||
|
||||
def on_disable():
|
||||
with open("/sys/class/hwmon/hwmon5/recalculate", "w") as f:
|
||||
f.write("0")
|
||||
# TODO restart system fan control
|
||||
|
||||
def on_set_enable(enable):
|
||||
if enable:
|
||||
on_enable()
|
||||
else:
|
||||
on_disable()
|
||||
|
|
160
main_view.html
160
main_view.html
|
@ -5,10 +5,164 @@
|
|||
<link rel="stylesheet" href="/steam_resource/css/library.css">
|
||||
<script src="/static/library.js"></script>
|
||||
<script>
|
||||
async function onload() {}
|
||||
const PLOT_HEIGHT = 200;
|
||||
const PLOT_WIDTH = 284;
|
||||
const OFFSET_X = 0;
|
||||
const OFFSET_Y = 0;
|
||||
// state
|
||||
let curve = [];
|
||||
let plotClickIsHandled = false;
|
||||
|
||||
// back-end
|
||||
|
||||
function setCurve(curve) {
|
||||
return call_plugin_method("set_curve", {"curve": curve});
|
||||
}
|
||||
|
||||
function getCurve() {
|
||||
return call_plugin_method("get_curve", {});
|
||||
}
|
||||
|
||||
function getCurvePoint(index) {
|
||||
return call_plugin_method("get_curve_point", {"index": index});
|
||||
}
|
||||
|
||||
function setCurvePoint(index, point) {
|
||||
return call_plugin_method("set_curve_point", {"index": index, "point": point});
|
||||
}
|
||||
|
||||
function addCurvePoint(point) {
|
||||
return call_plugin_method("add_curve_point", {"point": point});
|
||||
}
|
||||
|
||||
function removeCurvePoint(index) {
|
||||
return call_plugin_method("remove_curve_point", {"index": index});
|
||||
}
|
||||
|
||||
function setEnable(enable) {
|
||||
return call_plugin_method("set_enable", {"enable": enable});
|
||||
}
|
||||
|
||||
function getEnable() {
|
||||
return call_plugin_method("get_enable", {});
|
||||
}
|
||||
|
||||
function setPlotSize(x, y) {
|
||||
return call_plugin_method("set_plot_size", {"x": x, "y": y});
|
||||
}
|
||||
|
||||
// events
|
||||
|
||||
async function onload_body() {
|
||||
let graphDiv = document.getElementById("graphDiv");
|
||||
await setPlotSize(PLOT_WIDTH, PLOT_HEIGHT);
|
||||
const state_controlToggle = await getEnable(); // retrieve from back-end
|
||||
setToggleState(document.getElementById("controlToggle"), state_controlToggle);
|
||||
showHideElement(graphDiv, state_controlToggle);
|
||||
if (state_controlToggle) {
|
||||
curve = await getCurve();
|
||||
buildCurvePlot(curve);
|
||||
}
|
||||
console.log("Loaded");
|
||||
}
|
||||
|
||||
async function onclick_graphDiv(e) {
|
||||
console.log("Click @ (" + e.layerX.toString() + ", " + e.layerY.toString() + ")");
|
||||
if (plotClickIsHandled) {
|
||||
plotClickIsHandled = false;
|
||||
} else {
|
||||
await addCurvePoint({"x": (e.layerX - OFFSET_X) / PLOT_WIDTH, "y": (e.layerY - OFFSET_Y) / PLOT_HEIGHT});
|
||||
curve = await getCurve();
|
||||
buildCurvePlot(curve);
|
||||
}
|
||||
}
|
||||
|
||||
async function onclick_controlToggle() {
|
||||
console.log("Click @ controlToggle");
|
||||
let graphDiv = document.getElementById("graphDiv");
|
||||
let controlToggle = document.getElementById("controlToggle");
|
||||
const state_controlToggle = getToggleState(controlToggle);
|
||||
await setEnable(!state_controlToggle); // notify back-end
|
||||
setToggleState(controlToggle, !state_controlToggle);
|
||||
showHideElement(graphDiv, !state_controlToggle);
|
||||
if (!state_controlToggle) {
|
||||
curve = await getCurve();
|
||||
buildCurvePlot(curve);
|
||||
}
|
||||
}
|
||||
|
||||
async function onclick_plotPoint(e, index) {
|
||||
console.log("Click @ plotPoint " + index.toString());
|
||||
plotClickIsHandled = true; // this must be before the first async call (janky!)
|
||||
await removeCurvePoint(index);
|
||||
curve = await getCurve();
|
||||
buildCurvePlot(curve);
|
||||
//e.stopPropogation();
|
||||
}
|
||||
|
||||
// common
|
||||
|
||||
function buildCurvePlot(curve_points) {
|
||||
// TODO
|
||||
let graphDiv = document.getElementById("graphDiv");
|
||||
let newStr = "<span style=\"font-size:x-small;position:absolute;left:0px;top:0px;\">100%</span><span style=\"font-size:x-small;position:absolute;left:0px;bottom:0px;\">0</span><span style=\"font-size:x-small;position:absolute;left:0px;bottom:50%;writing-mode:vertical-lr;text-orientation:mixed;\">Fan</span><span style=\"font-size:x-small;position:absolute;right:0px;bottom:0px;\">100</span><span style=\"font-size:x-small;position:absolute;left:35%;bottom:0px;\">Temperature (C)</span>";
|
||||
for (let i = 0; i < curve_points.length; i++) {
|
||||
const point = curve_points[i];
|
||||
newStr += "<span style=\"position:absolute;"
|
||||
newStr += "top:" + Math.round(point["y"]*PLOT_HEIGHT + OFFSET_Y).toString() + "px;left:" + Math.round(point["x"]*PLOT_WIDTH + OFFSET_X).toString() + "px;";
|
||||
newStr += "width:8px;height:8px;background-color:red;\" id=\"plotPoint";
|
||||
newStr += i.toString() + "\" onclick=\"onclick_plotPoint(event," + i.toString() + ")\"></span>";
|
||||
}
|
||||
graphDiv.innerHTML = newStr;
|
||||
}
|
||||
|
||||
function showHideElement(elem, visible) {
|
||||
if (visible) {
|
||||
elem.style.visibility = "visible";
|
||||
} else {
|
||||
elem.style.visibility = "hidden";
|
||||
}
|
||||
}
|
||||
|
||||
function setToggleState(toggle, state) {
|
||||
const ENABLED_CLASS = "gamepaddialog_On_yLrDA";
|
||||
if (state && !toggle.classList.contains(ENABLED_CLASS)) {
|
||||
toggle.classList.add(ENABLED_CLASS);
|
||||
}
|
||||
|
||||
if (!state && toggle.classList.contains(ENABLED_CLASS)) {
|
||||
toggle.classList.remove(ENABLED_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
function getToggleState(toggle) {
|
||||
return toggle.classList.contains("gamepaddialog_On_yLrDA");
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body onload="onload()">
|
||||
<h2>Hello World</h2>
|
||||
<body onload="onload_body()">
|
||||
<div class="quickaccessmenu_TabGroupPanel_1QO7b Panel Focusable">
|
||||
<div class="quickaccesscontrols_PanelSectionRow_26R5w">
|
||||
<div class="quickaccesscontrols_PanelSectionRow_26R5w">
|
||||
<div class="gamepaddialog_Field_eKmEX gamepaddialog_WithFirstRow_2bDqk gamepaddialog_ExtraPaddingOnChildrenBelow_3nLNL gamepaddialog_StandardPadding_xIITX gamepaddialog_HighlightOnFocus_2HFrm Panel Focusable" style="--indent-level:0;">
|
||||
<div class="gamepaddialog_FieldLabelRow_2VcTl">
|
||||
<div class="gamepaddialog_FieldLabel_3jMlJ">
|
||||
Custom Fan Curve
|
||||
</div>
|
||||
<div class="gamepaddialog_FieldChildren_2rhav">
|
||||
<div id="controlToggle" tabindex="0" class="gamepaddialog_Toggle_9Ql-o Focusable" onclick="onclick_controlToggle()">
|
||||
<div class="gamepaddialog_ToggleRail_2bl0i"></div>
|
||||
<div class="gamepaddialog_ToggleSwitch_1PQpp"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gamepaddialog_FieldDescription_1W1to">Overrides SteamOS fan curve</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="graphDiv" style="height:200px;width:284px;border:2px solid red;position:relative;" onclick="onclick_graphDiv(event)">
|
||||
Some text to show that something is broken :(
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
Loading…
Reference in a new issue