PiFanTUI/main.py
2025-12-18 17:02:55 +01:00

116 lines
4.7 KiB
Python

#!/usr/bin/python3
import difflib
import tempfile
import shutil
import os
class DtParam:
__slots__ = ('temp', 'hyst', 'speed', 'N')
def __init__(self, temp : int, hyst : int, speed : int, n: int):
self.temp = temp # Temperature in MilliCelsius
self.hyst = hyst # Temperature Hysteresis in MilliCelsius
self.speed = speed # Fan Speed in 0-255
self.N = n # Fan Threshold number
self._assert_valid()
def _assert_valid(self):
assert self.temp // 1000 > 0 and self.temp // 1000 < 100, f"Temperature threshold not in sensible range (0, 100) C, found {self.temp}"
assert self.hyst // 1000 > 0 and self.hyst // 1000 < 100, f"Temperature hysteresis threshold not in sensible range (0, 100) C, found {self.temp}"
assert self.speed >= 0 and self.speed <= 255, f"Fan speed not in value range (0, 255), found {self.speed}"
assert self.N >= 0 and self.N <= 3, f"Threshold number not in supported set (0, 1, 2, 3), found {self.N}"
def __str__(self):
return f"dtparam=fan_temp{self.N}={self.temp},fan_temp{self.N}_hyst={self.hyst},fan_temp{self.N}_speed={self.speed}"
class FanCurve:
def __init__(self):
self.config_path = "/tmp/boot/firmware/config.txt"
self.settings = self._get_current_settings()
self._assert_valid_cfg()
def update(self, settings: list[DtParam]):
self.settings = settings
self.settings.sort(key=lambda s: s.temp)
for i, setting in enumerate(self.settings):
setting.N = i
self._assert_valid_cfg()
self._write_cfg()
def _write_cfg(self, dry_run: bool = False):
with open(self.config_path, "r") as f:
lines = f.readlines()
old_cfg = [line for line in lines if not (line.startswith("dtparam=fan_temp") or line.startswith("# Auto-Generated Fan Curve"))]
insert_at = None
for i, line in enumerate(old_cfg):
if line.strip() == "[all]":
insert_at = i
new_lines = ["# Auto-Generated Fan Curve parameters by PiFanUI\n"]
new_lines.extend([str(setting)+"\n" for setting in self.settings])
new_cfg = old_cfg[:insert_at + 1] + new_lines + old_cfg[insert_at + 1:]
if new_cfg == lines:
print("No new changes")
return
with tempfile.NamedTemporaryFile("w", dir="/tmp", delete=False) as new_conf:
new_conf.writelines(new_cfg)
diff = difflib.unified_diff(lines, new_cfg, fromfile=self.config_path, tofile=new_conf.name)
if dry_run:
user_dir = os.getcwd()
with open(os.path.join(user_dir, 'PIFanTUI.patch'), 'w') as patch_file:
patch_file.writelines(diff)
print(f"Patch file generated at {os.path.join(user_dir, 'PIFanTUI.patch')}\nrun '/boot/firmware/config.txt < PIFanTUI.patch' to apply the patch")
print(f"Updated values:")
for line in diff:
print(line)
if not dry_run:
shutil.move(new_conf.name, self.config_path)
def _assert_valid_cfg(self):
assert len(self.settings) <= 4 and len(self.settings) >= 1, f"Only up to 4 thresholds supported, found {len(self.settings)}"
assert sorted(self.settings, key=lambda s:s.temp) == sorted(self.settings, key=lambda s:s.N), f"Thresholds are not ordered correctly:\n{[str(s) for s in self.settings]}"
def _get_default_settings(self) -> list[DtParam]:
settings = []
settings.append(DtParam(temp=55000, hyst=2000, speed=90, n=0))
settings.append(DtParam(temp=65000, hyst=3000, speed=160, n=1))
settings.append(DtParam(temp=70000, hyst=4000, speed=200, n=2))
settings.append(DtParam(temp=72000, hyst=5000, speed=255, n=3))
return settings
def _get_current_settings(self) -> list[DtParam]:
settings = []
with open(self.config_path, "r") as f:
lines = f.readlines()
config_lines = [line.replace(" ", "").lower().split("#")[0][8:] for line in lines if line.startswith("dtparam=fan_temp")]
config_lines = sorted([item[8:].replace("_", "=") for string in config_lines for item in string.split(',')])
cfg = [item[2:].rstrip('\n') for item in config_lines]
# TODO if not specified fill in with defaults
assert len(cfg) % 3 == 0, "Partially Specified Fan Behaviour found, make sure to specify _temp, _hyst & _speed"
for i in range(0, len(config_lines), 3):
settings.append(DtParam(temp=int(cfg[i]), hyst=int(cfg[i+1][5:]), speed=int(cfg[i+2][6:]), n=i//3))
return settings
def main():
from tui import TUI
sc = FanCurve()
tui = TUI(sc)
tui.run()
if __name__ == "__main__":
main()