#!/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()