A safety-focused cross-platform fan-control daemon and native desktop workstation for compatible Clevo/Tongfang systems on Linux and Windows.
Important
Unofficial community project. This software is not affiliated with, endorsed by, sponsored by, or supported by Clevo, Tongfang, any laptop reseller, or any kernel-driver vendor. Use it at your own risk.
Fan Control provides automatic temperature curves, direct manual control,
live CPU/GPU telemetry, dual-axis history graphs, and a native desktop
window. The dashboard runs as a native desktop application (GTK4 + WebKitGTK 6
on Linux, Edge WebView2 / pywebview on Windows) communicating with the daemon
over a local control channel (Unix socket on Linux; the named pipe
\\.\pipe\fan-control-rpc on Windows); it does not require
external cloud services.
- Native desktop window (WebKit custom scheme on Linux; loopback bridge bound to 127.0.0.1 on Windows)
- Silent, Balanced, Performance, custom-curve, manual, and firmware-auto modes
- Independent or linked CPU/GPU fan curves and targets
- Visual curve editor with named import/export
- Critical-temperature override that bypasses the user noise cap
- Curve hysteresis to prevent rapid speed hunting
- Automatic handoff to firmware when temperature data becomes unavailable
- Live hwmon, ACPI thermal zone, and Uniwill EC sensors with an NVIDIA
nvidia-smifallback and optional pinning - Thirty-minute live telemetry and SQLite history with retention and CSV export
- Fan tachometer (RPM) readouts where the backend exposes them, including the Uniwill/Tongfang EC tach on Windows
- Persistent configuration with atomic writes
- Dark/light/system themes, Celsius/Fahrenheit display, desktop notifications, tray controls,
fan-ctlCLI - Dedicated Overview, Fans, Curves, Sensors, Analytics, Automation, Settings, and Diagnostics pages
- Revision-checked edits, per-fan policies, temporary fan tests, and explainable control decisions
- Transient automation rules and weekly schedules, including overnight blocks
- Hardware-free demo mode for safe evaluation and UI development
Fan Control supports hardware backends on both Linux and Windows. It auto-detects which
one is present; use --backend or FAN_CONTROL_BACKEND to override.
| Requirement | Details |
|---|---|
| Hardware | Clevo/Tongfang-based system with a compatible EC interface |
| Linux Backends | tuxedo_io (ioctls on a /dev/*_io device) or clevo_acpi (sysfs) |
| Windows Backends | windows_ec (direct EC port I/O 0x62/0x66 via InpOutx64/WinRing0) or windows_wmi (Uniwill/Tongfang EC over ACPI WMI AcpiTest_MULong) |
| Linux Desktop | GTK 4 and WebKitGTK 6 (gir1.2-gtk-4.0, gir1.2-webkit-6.0, python3-gi) |
| Windows Desktop | Edge WebView2 (native app mode or via pywebview), with tray via pystray |
| Runtime | Python 3.10 or newer |
| Privileges | Root (Linux) or Administrator (Windows) for the daemon; desktop dashboard runs unprivileged |
| Service manager | systemd (Linux) or Windows Task Scheduler / Service (Windows) |
| Telemetry | Linux: hwmon sysfs. Windows: ACPI WMI thermal zones and Uniwill EC temperatures. Both: optional nvidia-smi |
On Linux, the tuxedo_io backend drives duty through ioctls on a character device
matching /dev/*_io. The clevo_acpi backend drives per-fan duty through
plain sysfs files under /sys/class/leds/clevo-acpi::kbd_backlight/device/,
and relies on that driver's kernel-side watchdog to return control to
firmware auto if the controlling process stops.
On Windows, the windows_ec backend communicates directly with the Embedded Controller
(ports 0x66 command/status and 0x62 data) via standard I/O helper libraries
(inpoutx64.dll or WinRing0x64.dll). The windows_wmi backend talks to the
Uniwill/Tongfang EC through the ACPI WMI AcpiTest_MULong interface (GetSetULong),
the same channel the OEM control center uses. It provides EC temperatures, fan
tachometers, and manual duty control on the EC's 0-200 scale; the daemon must run
as Administrator, and pythonnet is required (pip install -r requirements-windows.txt).
If no hardware driver is present, the daemon reports the actionable error instead
of pretending to control fans.
Hardware compatibility varies by model and firmware. Start with demo mode, then verify sensor readings and fan response before enabling the service.
Fan Control treats thermal control as a safety-critical path:
- Duty is authored as a percentage 0-100. The
tuxedo_iobackend maps this onto its native raw 0-198 domain at the edge, keeping the known-safe cap of 198; values near 200 are known to behave unpredictably on affected firmware. - At
critical_temp, both fans are commanded to 100% even when a lower noise cap is configured. - After three invalid temperature readings, the daemon returns control to the system firmware until valid telemetry returns.
- The consent gate: firmware also owns the fans at the start of every daemon
session. A saved
manualorcurvemode is deliberately not applied on its own after a restart, so the EC stays on the firmware's curve until a mode is set again in that session.live_snapshot.control_engagedreports whether this session has taken control, andlive_snapshot.consent_pendingis the loop's own suppression predicate: true exactly while configured duties are being skipped for want of consent, false as soon as a rule or schedule drives the fans instead. The dashboard notice and thefan-ctl statushint both report that predicate verbatim, so neither can claim firmware control while your automation is the one running the fans — a duty that does not match the configured mode is the gate, not a bug. - On the
clevo_acpibackend, the kernel-side watchdog independently releases to firmware auto if the controlling process stops renewing a manual override. - On the Windows backends (
windows_ec,windows_wmi) the watchdog lives in the daemon: ten seconds without either a duty commit or a keep-alive ping while manual control is asserted releases the EC to firmware auto. The controller pings on every tick that has nothing new to write, so a settled target stays under manual control. A hard process kill cannot run that release, which is why the Windows task installer clears the manual bit explicitly on stop and uninstall. - The background daemon (
fan-daemon) is the sole root/Administrator owner of the EC interface and runs continuously. - The dashboard window runs unprivileged as the desktop user. On Linux, WebKit
never runs as root; it communicates with
fan-daemonover a local Unix socket restricted to thefan-controlgroup (0660 root:fan-control). On Windows, the dashboard serves the bundled UI over a loopback-only HTTP bridge bound to127.0.0.1and talks to the elevated daemon over the local control channel published in%PROGRAMDATA%\fan-control\run. - Safety-critical policy lives in Python (
fan_policy.py), not in JavaScript.
Caution
Confirm the reported temperatures and physical fan response on your exact machine. Incorrect low-level fan control can cause overheating or hardware damage.
GitHub Releases
ships the Linux AppImage, the Windows executables, and the source tarball; no
distribution package is published yet. Debian packaging lives in
packaging/debian/ for anyone building a .deb from a release tarball, and
sudo make install installs from source into /usr/local (it needs Node.js to
rebuild the dashboard bundle).
Add your user to the fan-control group afterwards:
sudo usermod -aG fan-control $USERfan-daemon is enabled and started on install (debhelper's systemd helpers in a
.deb, the shipped unit otherwise); the group change is what lets your user talk
to it, and it takes effect at your next login.
Each release ships:
-
Windows:
fan-control.exe(dashboard),fan-ctl.exe(CLI), andfan-daemon.exe(background daemon) as self-contained one-file executables. No Python installation is required. Runfan-daemon.exefrom an Administrator prompt for hardware control, or install it as a startup task withscripts\install-service-windows.ps1. -
Linux:
fan-control-<version>-x86_64.AppImagebundles the full GTK4 + WebKitGTK 6 dashboard (CPython, PyGObject, GTK, WebKit and the built UI). The dashboard is unprivileged; start the root daemon with thedaemonmode:chmod +x fan-control-2.0.1-x86_64.AppImage ./fan-control-2.0.1-x86_64.AppImage sudo ./fan-control-2.0.1-x86_64.AppImage daemon ./fan-control-2.0.1-x86_64.AppImage ctl status --json
An AppImage cannot create system groups, so the dashboard needs the group the packaged builds ship as a
sysusers.dfile, and a session restart after joining it:sudo groupadd --system fan-control sudo usermod -aG fan-control "$USER"Until then the daemon still runs, but the dashboard reports a permission error naming the group instead of connecting.
-
SHA256SUMS.txtlists the checksum of every attached asset, includingfan-control-<version>.tar.gz, the treemake distarchives from the tag thatpackaging/fan-control.specbuilds from. GitHub also provides its own source archives for the tag.
Debian/Kali:
sudo apt install python3 python3-gi gir1.2-gtk-4.0 gir1.2-webkit-6.0 nodejs npmFedora: python3-gobject gtk4 webkitgtk6.0 nodejs.
Arch: python-gobject gtk4 webkitgtk-6.0 nodejs npm.
Building the UI requires Node.js 20 or newer. Clone, build the UI, and install:
git clone https://github.com/vindeckyy/fan-control.git
cd fan-control
sudo make install
sudo systemd-sysusers /usr/lib/sysusers.d/fan-control.conf
sudo systemd-tmpfiles --create /usr/lib/tmpfiles.d/fan-control.conf
sudo usermod -aG fan-control $USER
sudo systemctl daemon-reload
sudo systemctl enable --now fan-daemon
Check the service after installation:
systemctl status fan-daemon
journalctl -u fan-daemon -n 50 --no-pagerOn Windows 10/11, Python 3.10+ is supported. IPC uses the named pipe
\\.\pipe\fan-control-rpc, whose name is published in
%PROGRAMDATA%\fan-control\run\control.sock (the default name is recorded as the
literal pipe; a daemon started with --pipe-name publishes that name instead,
so clients given only the run directory still find it).
A loopback-only TCP socket is used only when the Python build exposes neither
AF_UNIX nor named pipes, and it carries no extra authentication, so any local
process can reach the daemon exactly as the unprivileged dashboard does.
Native 64-bit Windows PE executables are provided:
fan-control.exe(andfan-gui.exe): Native Windows GUI launcher (windowed subsystem, no flashing console window) with high-res icon and DPI-aware manifest. Launches the dashboard directly.fan-ctl.exe: Native Windows console CLI executable for status, curves, fans, and diagnostics.fan-daemon.exe: Native Windows daemon executable for background thermal control.
These launchers automatically detect portable Python (python/pythonw.exe), virtual environments (.venv/), system Python, or Microsoft Store Python.
Double-click fan-control.exe, or from command line:
fan-control.exe
:: Hardware-free demo mode:
fan-control.exe --demo(Or use scripts\run-gui-windows.bat)
fan-ctl.exe status --json
fan-ctl.exe profile silent
fan-ctl.exe diagnoseTo control physical hardware, run the daemon from an elevated command prompt
(Administrator). Auto-detection selects windows_wmi on Uniwill/Tongfang
models; fan-daemon.exe --backend windows_wmi may be used explicitly:
fan-daemon.exe
fan-daemon.exe --backend windows_wmi
fan-daemon.exe --dry-run(Or use scripts\run-daemon-windows.bat)
To have the fan-control daemon start automatically on Windows boot with Administrator privileges without showing a terminal window, run PowerShell as Administrator:
powershell -ExecutionPolicy Bypass -File scripts\install-service-windows.ps1(To uninstall: powershell -ExecutionPolicy Bypass -File scripts\install-service-windows.ps1 -Uninstall)
The installer prefers the project .venv Python, installs pythonnet from
requirements-windows.txt if it is missing, registers the FanControlDaemon
scheduled task to run as SYSTEM at startup, and starts it immediately. On
uninstall it releases manual EC fan control back to firmware before removing
the task.
- Recompile Native Launchers: Run
make windows-exe(usesx86_64-w64-mingw32-gccandwindres). - Create Standalone Portable Package: Run
scripts\package-portable-windows.batto download official Python 3.12 embeddable and produce a zero-dependencydist\fan-control-portable\folder. - Build PyInstaller Bundle: Run
scripts\build-exe.baton Windows (usespackaging\windows\fan-control-pyinstaller.spec) to build frozen binaries indist\fan-control-windows\.
The daemon must run elevated (Administrator) to control fans. On Uniwill/Tongfang
models it uses the ACPI WMI AcpiTest_MULong interface via pythonnet
(pip install -r requirements-windows.txt). An error like "cannot access the
Uniwill EC over WMI" means the daemon is not elevated; install it with
scripts\install-service-windows.ps1 or start it from an Administrator terminal.
Alternatively, the windows_ec backend can drive the EC directly when a standard
user-mode I/O DLL (inpoutx64.dll or WinRing0x64.dll) is placed in
%SystemRoot%\System32 or the project root. Use --backend demo for evaluation
without hardware.
Open the native dashboard (runs unprivileged; no sudo needed):
fan-guifan-daemon remains running in the background while the window is open and
continues managing the embedded controller.
Preview without root or compatible hardware:
python3 fan-gui.py --demoHeadless snapshot for CI and scripts:
python3 fan-gui.py --demo --headless-smokeDisplay-only tray (never locks the EC; profile changes route over the socket):
fan-gui --trayCLI:
fan-ctl status --json
fan-ctl profile silent
fan-ctl profile cycle # silent -> balanced -> performance -> silent
fan-ctl profile cycle silent performance # restrict the rotation to a subset
fan-ctl mode released
fan-ctl set 1 40
fan-ctl cap 80
fan-ctl config hysteresis=3 linked=false
fan-ctl curve cpu
fan-ctl curves
fan-ctl curves load quiet
fan-ctl diagnoseprofile cycle is a single atomic RPC, so it is safe to bind to a physical
button. On Tongfang/Clevo-based chassis (e.g. Gateway Creator Series) the mode
button next to the power switch emits a key chord through the vendor keyboard
input device — for example Super+Alt+F6 on GK5NP5O — which a desktop
environment keybinding can map to fan-ctl profile cycle, optionally piping
the printed profile name into notify-send for an on-screen confirmation.
Run the daemon directly:
sudo fan-daemon
fan-daemon --dry-run
fan-daemon --diagnoseThe daemon owns /etc/fan-control.json on Linux (or %PROGRAMDATA%\fan-control\fan-control.json on Windows). Desktop and CLI controls submit RPC
mutations; they do not write this file directly. On Linux, the daemon reloads administrator
edits on SIGHUP / systemctl reload fan-daemon.
Version 2 stores fan policies, curves, rules, schedules, safety, display, and
history preferences in separate sections. Every saved mutation increments
revision. A client can send expected_revision to reject stale edits.
Existing v1 files migrate on load, with a *.v1.backup.json preserved before
the first v2 write. The following flat fields remain supported by the legacy
RPC adapters; they are not the v2 disk schema. See the v2 plan
for the schema and migration contract.
{
"profile": "balanced",
"mode": "curve",
"max_duty": 100,
"hysteresis": 5,
"critical_temp": 95,
"linked": true
}| Key | Default | Purpose |
|---|---|---|
profile |
balanced |
Active automatic curve |
mode |
released |
manual, curve, or released |
curve |
built-in | Shared [temperature, duty] points |
curve_cpu / curve_gpu |
none | Independent curves when linked is false |
max_duty |
100 |
Normal-operation noise cap, as a percentage |
hysteresis |
5 |
Minimum duty change before curve updates (explicit setpoints — fan tests and rule set_duty — are always written) |
critical_temp |
95 |
Temperature that forces maximum safe duty |
linked |
true |
Drive both fans from the same curve/target |
named_curves |
{} |
Saved custom curves |
cpu_sensor / gpu_sensor |
hottest defaults | Pinned sensor id from sensors.list (hwmon:k10temp:…:temp1, ec:temp1, nvidia:00000000:01:00.0); a legacy {name, label} pin is still accepted |
theme |
dark |
dark or light |
alerts.desktop |
false |
Desktop notifications at critical temperature |
Backend selection is automatic. Set FAN_CONTROL_BACKEND or pass --backend
to override. On Windows, available backends include windows_ec, windows_wmi, and demo.
The v2 control path is fan_controller.py → pure fan_engine.py → backend.
fan_rules.py computes temporary rule and schedule overlays. fan_history.py
stores normalized telemetry in SQLite. Every decision records its source,
requested duty, limits, final duty, and whether the backend write succeeded.
The React workspace uses hash routing, a typed client, and separate stores
for connection, configuration, live telemetry, and presentation state.
hwmon / WMI / NVIDIA telemetry ──► temperature selection ──► curve + safety policy
│
▼
┌────── clevo_acpi sysfs ───────┐ ◄── duty target ───┤
│ │ │
backend ┼──── tuxedo_io ioctls ─────────┤ ◄── percent duty ──┤
│ │ │
├──── windows_ec port I/O ──────┤ │
│ │ │
├──── windows_wmi EC (WMI) ─────┤ │
│ │ │
└──── demo (simulated) ─────────┘ │
│ │
▼ │
firmware auto ◄── ownership handoff ◄── watchdog / release
fan_backend.py: hardware backends (Linuxtuxedo_io/clevo_acpi, Windowswindows_ecdirect port I/O /windows_wmiUniwill EC over ACPI WMI, and cross-platformdemo).fan_ec_maps.py: per-barebone EC register maps. Manual writes are refused unless the chassis barebone ID is present, so an unverified model stays read-only on firmware automatic control.fan_policy.py: versioned configuration, migration, validation, curve math, and cross-platform sensor discovery.fan_engine.pyandfan_rules.py: pure control decisions, rules, and schedules.fan_history.py: SQLite persistence, query aggregation, and memory fallback.fan_controller.py: controller logic and JSON-RPC dispatch methods.fan_rpc.py: JSON-RPC server and client (Unix socket on Linux, named pipe on Windows, loopback TCP as a last-resort fallback).fan_diagnostics.py: system and hardware diagnostics for Linux and Windows.fan_alerts.pyandfan_hostops.py: the shared alert loop (critical temperature, rule and schedule overlays) and the host operations both shells use (clipboard, file dialogs, notification delivery, diagnostic export).fan_gtk.py: unprivileged GTK4 + WebKitGTK 6 workspace and tray for Linux.fan_windows_gui.py: native unprivileged desktop workspace (Edge WebView2 / pywebview) and tray (pystray) for Windows.fan-gui.py: GUI entry point (--demo,--tray,--headless-smoke) dispatching to Linux GTK or Windows native runner.fan-daemon.py: background control service and sole EC owner.fan-ctl.py: CLI client communicating with the daemon over the local socket.ui/: React + Vite workspace loaded via native custom scheme (Linux) or loopback bridge (Windows).
Linux:
sudo fan-daemon --diagnose
sudo fan-ctl diagnoseWindows (from an Administrator terminal for EC access):
fan-daemon.exe --diagnose
fan-ctl.exe diagnoseThe daemon must run elevated to write the EC. Check that the daemon is reachable and using the hardware backend:
fan-ctl.exe capabilities --json
fan-ctl.exe status --jsoncapabilities should report "backend": "windows_wmi" (or windows_ec) and
status should show "daemon_reachable": true. If not, install the
background service (scripts\install-service-windows.ps1 from PowerShell as
Administrator) or start fan-daemon.exe elevated. An unelevated daemon now
fails with an actionable error instead of silently ignoring writes.
- Linux:
systemctl status fan-daemon - Windows: the control channel is published in
%PROGRAMDATA%\fan-control\run\control.sock(the named pipe\\.\pipe\fan-control-rpc); the scheduled taskFanControlDaemonshould be running. Start it withschtasks /run /tn FanControlDaemonfrom an elevated prompt, or reinstall the service.
Install GI bindings (gir1.2-gtk-4.0, gir1.2-webkit-6.0) and build the UI
(cd ui && npm ci && npm run build). Run python3 fan-gui.py --demo --debug
to enable the WebKit inspector. On Windows, fan-control.exe --demo runs
without hardware or Administrator access.
nvidia-smi --query-gpu=index,temperature.gpu,name --format=csv,noheader,nounitsdiagnostics.snapshot reports history health under its history key
(degraded, error, quarantined). If the SQLite
file cannot be read (power loss, disk fault, manual edit), the store keeps the
last 2000 samples in memory, retries, and after three failed probes moves the
file aside as history.corrupt-<timestamp>.db and rebuilds a fresh database, so
recording resumes by itself. The quarantined file is kept for inspection;
delete it once you no longer need it. history.persist=false disables disk
history entirely.
Timezone values in schedules and rule time windows are IANA names (local is
the default). Windows has no system zone database, so install tzdata
(pip install -r requirements-windows.txt) to use names other than local.
Linux:
python3 -m py_compile fan_*.py fan-*.py test_*.py
python3 -m unittest -v
cd ui && npm ci && npm test && npm run build
python3 fan-gui.py --demoWindows (PowerShell):
python -m unittest -v
python fan-gui.py --demo --headless-smoke
python -m PyInstaller --clean -y packaging\windows\fan-control-pyinstaller.specThe full Python suite runs on Windows without elevated access; hardware tests are skipped or mocked unless a daemon is installed.
See CONTRIBUTING.md before proposing hardware-facing changes. Security issues should follow SECURITY.md.
Clevo and Tongfang names are used only to describe hardware compatibility. All product names and trademarks belong to their respective owners. This repository provides no manufacturer warranty, certification, or support.
Overview shows temperatures, fan response, and effective policy. Fans edits independent policies and runs expiring tests. Curves edits reusable curves with local previews and explicit Save. Sensors selects control sources and history pins. Analytics queries persisted history. Automation tests rules and edits schedules. Settings contains safety and display preferences. Diagnostics explains individual writes and exports a report.
Ctrl+K opens page and control commands. In Manual mode, [ and ] adjust
the first fan target in 5% steps, following the configured legacy linking.
Curve points support arrow keys and Delete as well as pointer dragging.
Manual targets are floored at 15%. Lower values, including 0% from an older
client or a hand-edited config, are lifted before the EC sees them; the
workspace slider and fan-ctl set clamp to the floor that capabilities
publishes as manual_min_duty. The floor bounds the target, not the duty:
it is applied before max_duty scaling, so a fan capped at 50% runs at 8% for
a floored 15% target. Curve mode and rule set_duty overrides are not floored
— a curve may stop the fan at low temperature (bounded by min_duty /
max_duty) and a rule overlay is an explicit operator decision.
fan-ctl capabilities --json
fan-ctl fans list --json
fan-ctl fans configure fan1 'control={"type":"manual","target":45}' min_duty=20 max_duty=90
fan-ctl fans test fan1 delta=10 duration_ms=5000
fan-ctl curves list
fan-ctl curves assign fan1 cpu_default
fan-ctl sensors list
fan-ctl rules list
fan-ctl rules test gpu_warm
fan-ctl rules disable gpu_warm
fan-ctl history stats
fan-ctl history query max_points=600 'fans=["fan1"]'
fan-ctl diagnostics --jsonHistory defaults to /var/lib/fan-control/history.db on Linux (or %PROGRAMDATA%\fan-control\data\history.db on Windows) with seven-day retention.
Command execution through automation is not included. Package version 2.0.1
uses epoch 1 so it sorts after the previous calendar-version packages.
Linux:
make test
ruff check .
python3 scripts/check_versions.py
xvfb-run -a python3 scripts/gtk-smoke.py
python3 scripts/ui-demo.py
# In another terminal, with Chromium, chromedriver, and Python Selenium installed:
python3 scripts/ui-acceptance.pyWindows (PowerShell):
python -m unittest -v
ruff check .
python scripts\check_versions.py
python fan-gui.py --demo --headless-smokeThe acceptance harness uses isolated simulated hardware and produces
screenshots under docs/images/v2. It is not the application's runtime
transport. The native application continues to use the local control channel
(Unix socket on Linux, named pipe on Windows).

