Last active
July 29, 2024 18:33
-
-
Save jpsutton/c8bf024f89606f4840f365b24971af33 to your computer and use it in GitHub Desktop.
Simple fan controller in Python using Linux hwmon sensors and liquidctl
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.