Skip to content

Instantly share code, notes, and snippets.

@jpsutton
Last active July 29, 2024 18:33
Show Gist options
  • Save jpsutton/c8bf024f89606f4840f365b24971af33 to your computer and use it in GitHub Desktop.
Save jpsutton/c8bf024f89606f4840f365b24971af33 to your computer and use it in GitHub Desktop.
Simple fan controller in Python using Linux hwmon sensors and liquidctl
#!/usr/bin/env python
import sys
import json
import time
import subprocess
import statistics
"""
{ "sensor_low": 35, "sensor_high": 40, "fan_level": 30 },
{ "sensor_low": 39, "sensor_high": 43, "fan_level": 40 },
{ "sensor_low": 42, "sensor_high": 47, "fan_level": 50 },
{ "sensor_low": 46, "sensor_high": 50, "fan_level": 80 },
{ "sensor_low": 49, "sensor_high": 52, "fan_level": 100 },
"""
KMODULE = "it87"
CHIP = "it8686-isa-0a60"
SENSOR = "EC_TEMP"
THRESHOLDS = [
{ "sensor_low": 35, "sensor_high": 40, "fan_level": 30 },
{ "sensor_low": 39, "sensor_high": 43, "fan_level": 75 },
{ "sensor_low": 42, "sensor_high": 52, "fan_level": 100 },
]
HISTORY = list()
ROLLING_COUNT=10
FAN_LEVEL = THRESHOLDS[0]["fan_level"]
SAMPLING_FREQUENCY = 6
GAMEMODE_MAX = True
def gamemode_active():
p = subprocess.run(['ps', 'ax'], stdout=subprocess.PIPE)
return b"gamemoderun" in p.stdout
def get_sensor_data():
p = subprocess.run(['sensors', '-j', CHIP], stdout=subprocess.PIPE)
return json.loads(p.stdout)
def set_fan_level(level):
print(f"Set fan level to {level}")
subprocess.run(['liquidctl', 'set', 'fans', 'speed', str(level)])
def get_sensor_sample():
sensor_data = get_sensor_data()[CHIP][SENSOR]
for key, value in sensor_data.items():
if key.endswith("_input"):
return value
raise RuntimeError(f"Unable to locate sensor data value for sensor {SENSOR} on chip {CHIP}")
print(f"Loading kernel module '{KMODULE}'")
subprocess.run(['modprobe', KMODULE])
set_fan_level(THRESHOLDS[0]["fan_level"])
while True:
# Flush stdout stream for more responsive logging
sys.stdout.flush()
# Sleep X seconds between samplings
time.sleep(SAMPLING_FREQUENCY)
# If enabled, set fans to max when gamemode is active
if GAMEMODE_MAX and gamemode_active():
if FAN_LEVEL != 100:
print("Setting fans to max while gamemode is active")
set_fan_level(FAN_LEVEL := 100)
continue
# Read the current value and record it
sensor_value = get_sensor_sample()
HISTORY.append(sensor_value)
# Make sure we keep only as many data points as we care to average
if len(HISTORY) > ROLLING_COUNT:
HISTORY.pop(0)
else:
continue
# Calculate the rolling average
rolling_average = statistics.mean(HISTORY)
level_up = False
# Decide if we need to change the fan level
for threshold in THRESHOLDS:
if level_up or (FAN_LEVEL > threshold["fan_level"] and rolling_average < threshold["sensor_low"]):
FAN_LEVEL = threshold["fan_level"]
set_fan_level(FAN_LEVEL)
print(f"Fan level: {FAN_LEVEL}")
print(f"Sensor rolling avg: {rolling_average}")
break
elif FAN_LEVEL == threshold["fan_level"] and rolling_average > threshold["sensor_high"]:
level_up = True
@jpsutton
Copy link
Author

I wrote this for my own setup. The pump and 4 fans are plugged into a Corsair Commander Core XT, which liquidctl can support on Linux. I have a water temperature sensor at the outlet of the pump. That is the sensor being measured to make fan/pump speed decisions.

If it's useful to someone else, cool. Otherwise, this is just out here to be archived in case I reinstall and forget to copy it off my system.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment