A PyQt6 desktop app for controlling fans, thermals, CPU power limits, and battery
charge thresholds on Lenovo ThinkPads under Linux, via thinkpad_acpi and standard
kernel sysfs. Manual level, temperature curve, or closed-loop "hold this temperature"
thermostat — with a live temp/RPM graph and AC/battery-aware profiles.
- Three modes — Manual (pin a fixed level), Curve (temp → level with hysteresis, preset or custom), and Target (thermostat that adapts the fan to the workload).
- AC / battery-aware — separate curves and power presets, switched automatically on unplug.
- CPU power limits (Intel RAPL) — PL1/PL2 presets (Quiet → Max), with a built-in benchmark that runs a sustained load at each preset and compares steady-state clock/temp.
- Intel iGPU frequency caps — min/max clock limits with Quiet/Balanced/Performance/Max
presets, optional auto-cap on battery, and a 15-second keep-or-revert confirmation on every
change. Busy% is read from RC6 idle residency, so it works without root
(
intel_gpu_topneedsCAP_PERFMON). Note: most laptops already ship at the hardware ceiling, so this can only cap downward — for heat, noise, and battery, not extra speed. - Battery longevity — set charge start/stop thresholds.
- Live graph — temps, fan RPM, and measured package power draw; CSV export.
- Auto-profile — detects games / compiles / AI workloads and switches presets.
- Critical-temp protection — forces full-speed above a threshold, independent of the curve.
- Optional telemetry — push metrics to a Prometheus/VictoriaMetrics endpoint and fire ntfy alerts on critical temps. Off by default; no endpoints preconfigured.
- A Lenovo ThinkPad with the
thinkpad_acpikernel module (standard on Linux). - Python 3 + PyQt6 (
pip install PyQt6). pkexec/sudofor the one-time privileged setup.- Optional:
stress-ngfor the in-app benchmark.
Tested with app version 2.2 on:
| ThinkPad model | CPU generation | Kernel | Distro |
|---|---|---|---|
| P1 Gen 3 | Intel 10th-gen (Comet Lake-H) | Linux 6.12 | Debian 13 |
| P1 Gen 4 | Intel 11th-gen (Tiger Lake-H) | Linux 7.1 (zen) | Arch |
Everything it drives — thinkpad_acpi fan control, Intel RAPL power limits,
platform_profile, BAT0 charge thresholds, and i915 gt_*_freq_mhz — is common across recent ThinkPads,
and per-chassis details (fan count, hwmon indices, PL ceilings) are auto-detected;
missing sensors are handled gracefully. Other models (incl. P1 Gen 2) very likely
work but are unverified — reports welcome. Nothing here is model-hardcoded.
git clone https://github.com/Scanner771/thinkpad-fan-control && cd thinkpad-fan-control
pip install -r requirements.txt
sudo bash setup-fan-control.sh
python3 fan-control.py
setup-fan-control.sh (run once) installs the fanctl privileged helper to
/usr/local/bin, grants passwordless sudo for that one command only, and persists
thinkpad_acpi fan_control=1 (the kernel gate that permits fan writes) via
/etc/modprobe.d/.
cp systemd/fan-control.service ~/.config/systemd/user/ && systemctl --user enable --now fan-control.service
sudo rm -f /usr/local/bin/fanctl /etc/sudoers.d/fan-control /etc/modprobe.d/thinkpad_acpi.conf && systemctl --user disable --now fan-control.service
That removes the helper, the passwordless-sudo grant, and the fan_control=1 persistence.
Fan control returns to firmware on the next reboot (or sudo modprobe -r thinkpad_acpi && sudo modprobe thinkpad_acpi). Settings in ~/.config/fan-control/ are left alone — delete
that directory too if you want a clean slate.
| Key | Action |
|---|---|
Ctrl+1 … Ctrl+5 |
jump to tab |
Ctrl+Tab / Ctrl+Shift+Tab |
cycle tabs |
Ctrl+R / F5 |
force a refresh |
Esc / Ctrl+W |
hide to tray (or close if there's no tray) |
Ctrl+Q |
quit |
The GUI only reads kernel sysfs directly (/sys/class/hwmon, /sys/class/powercap,
platform_profile, BAT0). Every privileged write goes through fanctl, a small
shell helper that validates its arguments before touching /proc/acpi/ibm/fan, the RAPL
power-limit files, the i915 GPU frequency nodes, or the battery thresholds — so the
passwordless-sudo grant is scoped to one auditable command, not to arbitrary root writes.
GPU writes are clamped to the hardware-reported RPn..RP0 range and the i915 card is
resolved by driver symlink rather than a hardcoded cardN (the number is not stable
across boots). Only one instance can run at a time (QLockFile), so an autostart entry
and a session restore can't end up both driving the fan.
Settings live in ~/.config/fan-control/config.json (curves, presets, PL limits,
telemetry). See config.example.json. The app rewrites this file on exit, so edit it
only while the app is stopped.
Disabled by default with no endpoints set. To enable, fill in telemetry.vm_url
(Prometheus/VictoriaMetrics import URL) and/or telemetry.ntfy_url + ntfy_topic,
and set telemetry.enabled: true.
This app writes fan levels and CPU power limits. Curves are pre-ramped (idle above a dead stop) so a fan isn't spooling from standstill under a sudden all-core load, and a critical-temperature failsafe forces full speed regardless of the active curve. GPU frequency caps are staged behind a 15-second keep-or-revert dialog and are clamped to the hardware range; nothing GPU-side persists, so a reboot always restores stock clocks. Still, you are overriding firmware defaults — understand your hardware's limits.
See CHANGELOG.md.
MIT — see LICENSE.




