Skip to content

Instantly share code, notes, and snippets.

@zhaoweizhong
Created September 1, 2025 15:34
Show Gist options
  • Save zhaoweizhong/cdea2b9476260e554ce43a6daf147967 to your computer and use it in GitHub Desktop.
Save zhaoweizhong/cdea2b9476260e554ce43a6daf147967 to your computer and use it in GitHub Desktop.
Temperature-based fan speed controller for Dell T630 (v2)
#!/usr/bin/env python3
import psutil
import subprocess
import sys
import time
import signal
import logging
# Configuration - hardcoded for simplicity
TEMP_THRESHOLDS = [45, 55, 65, 75] # °C
FAN_SPEEDS = [9, 10, 15, 20] # %
HYSTERESIS = 3 # °C
CHECK_INTERVAL = 30 # seconds
EMERGENCY_TEMP = 78 # °C - switch to automatic
# Global state
current_fan_speed = 0
current_mode = "automatic"
debug_mode = False
def setup_logging():
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
def ipmitool_cmd(cmd_args):
"""Execute ipmitool command with error handling"""
cmd = ["ipmitool"] + cmd_args.split()
if debug_mode:
logging.info(f"IPMI Command: {' '.join(cmd)}")
return True
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode != 0:
logging.error(f"IPMI command failed: {' '.join(cmd)} - {result.stderr}")
return False
return True
except subprocess.TimeoutExpired:
logging.error(f"IPMI command timeout: {' '.join(cmd)}")
return False
except Exception as e:
logging.error(f"IPMI command error: {e}")
return False
def set_fan_mode(mode):
"""Set fan control mode (manual/automatic)"""
global current_mode
if mode == current_mode:
return True
if mode == "manual":
logging.info("Switching to manual fan control")
success = ipmitool_cmd("raw 0x30 0x30 0x01 0x00")
elif mode == "automatic":
logging.info("Switching to automatic fan control")
success = ipmitool_cmd("raw 0x30 0x30 0x01 0x01")
else:
logging.error(f"Invalid fan mode: {mode}")
return False
if success:
current_mode = mode
if mode == "automatic":
global current_fan_speed
current_fan_speed = 0
return success
def set_fan_speed(speed_percent):
"""Set fan speed percentage (5-100%)"""
global current_fan_speed
if speed_percent == current_fan_speed:
return True
if not (5 <= speed_percent <= 100):
logging.error(f"Invalid fan speed: {speed_percent}%")
return False
# Always ensure manual mode before setting speed
if not set_fan_mode("manual"):
logging.error("Failed to switch to manual mode")
return False
time.sleep(2) # Give IPMI more time to switch modes
# Convert to hex
speed_hex = f"0x{speed_percent:02x}"
logging.info(f"Setting fan speed to {speed_percent}%")
success = ipmitool_cmd(f"raw 0x30 0x30 0x02 0xff {speed_hex}")
if success:
current_fan_speed = speed_percent
return success
def get_cpu_temperature():
"""Get average CPU package temperature"""
try:
temps = psutil.sensors_temperatures()
if 'coretemp' not in temps:
logging.error("No CPU temperature sensors found")
return None
# Get package temperatures only (ignore individual cores)
package_temps = []
for sensor in temps['coretemp']:
if 'Package id' in sensor.label:
package_temps.append(sensor.current)
if not package_temps:
logging.error("No CPU package temperature sensors found")
return None
avg_temp = sum(package_temps) / len(package_temps)
return round(avg_temp, 1)
except Exception as e:
logging.error(f"Error reading temperature: {e}")
return None
def check_hysteresis(temp, threshold_index):
"""Check if temperature change should trigger fan speed change"""
if threshold_index < 0 or threshold_index >= len(TEMP_THRESHOLDS):
return True
current_threshold = TEMP_THRESHOLDS[threshold_index]
target_speed = FAN_SPEEDS[threshold_index]
# If we want to increase fan speed, do it immediately
if current_fan_speed < target_speed:
return True
# If we want to decrease fan speed, apply hysteresis
if current_fan_speed > target_speed:
return temp <= (current_threshold - HYSTERESIS)
return True
def control_fans(temp):
"""Main fan control logic"""
if temp is None:
logging.warning("No temperature reading, keeping current settings")
return
# Emergency mode - very high temperature
if temp >= EMERGENCY_TEMP:
logging.warning(f"Emergency temperature {temp}°C! Switching to automatic")
set_fan_mode("automatic")
return
# Determine target fan speed based on temperature
target_speed = FAN_SPEEDS[0] # Default to lowest speed
threshold_index = 0
for i, threshold in enumerate(TEMP_THRESHOLDS):
if temp > threshold:
target_speed = FAN_SPEEDS[min(i + 1, len(FAN_SPEEDS) - 1)]
threshold_index = min(i + 1, len(FAN_SPEEDS) - 1)
# Apply hysteresis check
if check_hysteresis(temp, threshold_index):
set_fan_speed(target_speed)
logging.info(f"Temperature: {temp}°C, Fan: {current_fan_speed}%, Mode: {current_mode}")
def graceful_shutdown(signum, frame):
"""Handle shutdown signals"""
logging.info(f"Received signal {signum}, shutting down...")
logging.info("Switching fans back to automatic control")
set_fan_mode("automatic")
sys.exit(0)
def main():
global debug_mode
# Check for debug flag
if len(sys.argv) > 1 and sys.argv[1] == "--debug":
debug_mode = True
logging.getLogger().setLevel(logging.DEBUG)
logging.info("Debug mode enabled")
setup_logging()
# Setup signal handlers for graceful shutdown
signal.signal(signal.SIGTERM, graceful_shutdown)
signal.signal(signal.SIGINT, graceful_shutdown)
logging.info("Starting fan control service")
logging.info(f"Temperature thresholds: {TEMP_THRESHOLDS}°C")
logging.info(f"Fan speeds: {FAN_SPEEDS}%")
logging.info(f"Hysteresis: {HYSTERESIS}°C")
logging.info(f"Check interval: {CHECK_INTERVAL}s")
try:
while True:
temp = get_cpu_temperature()
control_fans(temp)
time.sleep(CHECK_INTERVAL)
except KeyboardInterrupt:
graceful_shutdown(signal.SIGINT, None)
except Exception as e:
logging.error(f"Unexpected error: {e}")
set_fan_mode("automatic")
sys.exit(1)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment