Skip to content

Instantly share code, notes, and snippets.

@James-Ansley
Last active April 15, 2025 02:15
Show Gist options
  • Save James-Ansley/32f72729487c8f287a801abcc7a54f38 to your computer and use it in GitHub Desktop.
Save James-Ansley/32f72729487c8f287a801abcc7a54f38 to your computer and use it in GitHub Desktop.
Raspberry Pi 5 Auto Fan Controller

Raspberry Pi 5 Auto Fan Controller

UPDATE: The fan is now controlled automatically in the latest updates (See this answer https://askubuntu.com/a/1497778/1746852). This cron job is no longer needed.

A quick hack to get around the fact Ubuntu for the Pi5 currently does not control the fan.

This needs to be run with sudo permissions:

sudo python3 pi5_fan_controller.py

And, by default, will monitor the Pi5's temperature for one minute every 2 seconds adjusting the fan based on some arbitrary boundaries I came up with on the spot :^)

This is intended to be set up as a system-wide cron job (system-wide because of the sudo privileges). To do this, edit this file:

sudo nano /etc/crontab

And add this cron job:

* * * * *  root  python3 /path/to/pi5_fan_controller.py

(with the updated path to the python file)

Many thanks to the following Ask Ubuntu answers:

from enum import Enum
import time
TEMP_PATH = "/sys/devices/virtual/thermal/thermal_zone0/temp"
FAN_PATH = "/sys/class/thermal/cooling_device0/cur_state"
class FanSpeed(Enum):
OFF = 0
LOW = 1
MEDIUM = 2
HIGH = 3
FULL = 4
def main():
start = time.time()
while time.time() - start < 59:
temp = get_temp()
if temp > 70:
speed = FanSpeed.FULL
elif temp > 65:
speed = FanSpeed.HIGH
elif temp > 60:
speed = FanSpeed.MEDIUM
elif temp > 50:
speed = FanSpeed.LOW
else:
speed = FanSpeed.OFF
set_fan_speed(speed)
time.sleep(2)
def get_temp() -> int:
with open(TEMP_PATH, "r") as f:
data = f.read()
return int(data) // 1000
def set_fan_speed(speed: FanSpeed):
with open(FAN_PATH, "w") as f:
f.write(str(speed.value))
if __name__ == "__main__":
main()
@leslieadams57
Copy link

Hi James. I have a NAS with a Raspberry Pi 5 and a built in fan/hat automatically controlled. The case I've made for the NAS also has a large (40mm) 2 phase case fan that is just on or off (for cooling the RPI5 and an NVME/PCie SSD + 2 USB3/SATA SSDs). What I'd planned to do is write some code to controlled this fan via GPIO and based on the RPI5 temperature. E.g fan strictly on or off as a back up to the on board fan/hat that now comes with the RPI5. Any thoughts ??? Your code is used via Cron rather than using a daemon. Any particular reason? Many thanks Les Adams ...

@13hakta
Copy link

13hakta commented Jan 18, 2024

Hi! I use this script for my R4B, according to script above my version will suit for R5 too.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import time
import sys
import os.path

# Configuration
WAIT_TIME = 3  # [s] Time to wait between each refresh
FAN_MIN = 30  # [%] Fan minimum speed.
PWM_FREQ = 10  # [Hz] Change this value if fan has strange behavior

# Configurable temperature and fan speed steps
tempSteps  = [40, 50, 70]  # [°C]
speedSteps = [20, 40, 100] # [%]

# Fan speed will change only of the difference of temperature is higher than hysteresis
hyst = 1

sys_path = "/sys/class/pwm/pwmchip0"
pwm_path = sys_path + "/pwm0"

def write_file(name: str, value: str):
    f = open(name, "w")
    f.write(value)
    f.close()

def create_pwm():
    global sys_path
    write_file(sys_path + "/export", "0")

def remove_pwm():
    global sys_path
    write_file(sys_path + "/unexport", "0")

def set_state(state):
    global pwm_path
    write_file(pwm_path + "/enable", str(state))

def set_pwm(freq, speed):
    global pwm_path

    period = int(1000000000 / freq)
    duty_cycle = int((period / 100) * speed)

    write_file(pwm_path + "/period", str(period))
    write_file(pwm_path + "/duty_cycle", str(duty_cycle))


if not os.path.exists(sys_path):
    print("PWM chip device not exists")
    sys.exit(1)

if not os.path.exists(pwm_path):
    print("Create PWM")
    create_pwm()

    if not os.path.exists(pwm_path):
        print("Unable to create PWM")
        sys.exit(2)

# Setup GPIO pin
set_pwm(PWM_FREQ, 0)
set_state(1)

i = 0
cpuTemp = 0
fanSpeed = 0
cpuTempOld = 0
fanSpeedOld = 0

# We must set a speed value for each temperature step
if len(speedSteps) != len(tempSteps):
    print("Numbers of temp steps and speed steps are different")
    exit(0)

try:
    while 1:
        # Read CPU temperature
        cpuTempFile = open("/sys/class/thermal/thermal_zone0/temp", "r")
        cpuTemp = float(cpuTempFile.read()) / 1000
        cpuTempFile.close()

        # Calculate desired fan speed
        if abs(cpuTemp - cpuTempOld) > hyst:
            # Below first value, fan will run at min speed.
            if cpuTemp < tempSteps[0]:
                fanSpeed = speedSteps[0]
            # Above last value, fan will run at max speed
            elif cpuTemp >= tempSteps[len(tempSteps) - 1]:
                fanSpeed = speedSteps[len(tempSteps) - 1]
            # If temperature is between 2 steps, fan speed is calculated by linear interpolation
            else:
                for i in range(0, len(tempSteps) - 1):
                    if (cpuTemp >= tempSteps[i]) and (cpuTemp < tempSteps[i + 1]):
                        fanSpeed = round((speedSteps[i + 1] - speedSteps[i])
                                         / (tempSteps[i + 1] - tempSteps[i])
                                         * (cpuTemp - tempSteps[i])
                                         + speedSteps[i], 1)

            if fanSpeed != fanSpeedOld:
                if (fanSpeed != fanSpeedOld
                        and (fanSpeed >= FAN_MIN or fanSpeed == 0)):

                    set_pwm(PWM_FREQ, fanSpeed)
                    fanSpeedOld = fanSpeed
            cpuTempOld = cpuTemp

        # Wait until next refresh
        time.sleep(WAIT_TIME)


# If a keyboard interrupt occurs (ctrl + c), the GPIO is set to 0 and the program exits.
except KeyboardInterrupt:
    print("Fan ctrl interrupted by keyboard")
    set_state(0)
    sys.exit()

I keep my script at /usr/local/bin, so you might want to put it there too.

Init script /etc/init.d/fancontrol.sh

#! /bin/sh

### BEGIN INIT INFO
# Provides:          fancontrol.py
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
### END INIT INFO

# Carry out specific functions when asked to by the system
case "$1" in
  start)
    echo "Starting fancontrol.py"
    /usr/local/bin/fancontrol.py &
    ;;
  stop)
    echo "Stopping fancontrol.py"
    pkill -f /usr/local/bin/fancontrol.py
    ;;
  *)
    echo "Usage: /etc/init.d/fancontrol.sh {start|stop}"
    exit 1
    ;;
esac

exit 0

@leslieadams57
Copy link

leslieadams57 commented Jan 21, 2024 via email

@13hakta
Copy link

13hakta commented Jan 21, 2024

You made a heavy work :) My RP4 has also 2-wired cooler, that is why I soldered couple components and made it PWM-compatible. My script above works as a service without cron.

@nicciniamh
Copy link

Hi, I took your code, made it a little more oop, and wrapped a systemd service around it along with a simple bash script to set it all up.
https://github.com/nicciniamh/pi5-fan-control

@Danrancan
Copy link

On Pi5, Argon One v3, Ubuntu 24.04:

git clone https://gist.github.com/32f72729487c8f287a801abcc7a54f38.git
cd 32f72729487c8f287a801abcc7a54f38
sudo python3 pi5_fan_controller.py
Traceback (most recent call last):
  File "/home/boopi/Git/32f72729487c8f287a801abcc7a54f38/pi5_fan_controller.py", line 46, in <module>
    main()
  File "/home/boopi/Git/32f72729487c8f287a801abcc7a54f38/pi5_fan_controller.py", line 30, in main
    set_fan_speed(speed)
  File "/home/boopi/Git/32f72729487c8f287a801abcc7a54f38/pi5_fan_controller.py", line 41, in set_fan_speed
    with open(FAN_PATH, "w") as f:
         ^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: '/sys/class/thermal/cooling_device0/cur_state'

Any advice?

@James-Ansley
Copy link
Author

Hi @Danrancan,

The Argon one fan controller uses a different file to control the fan speed since the Argon fan is connected to the Argon case and not directly to the Pi5 — from looking online, it seems Argon uses a file inside of etc/argon to manage fan speeds, etc. Some people also seem to have plugged the Argon fan directly into the Pi5 instead of connecting it to the case, which seems to improve fan speed management without the need for a script like this.

Since I don't have an Argon case, I cannot debug this further — but if you find the correct file to update the fan path, I'm sure other people would benefit from you commenting your solution here or from forking this gist.

Thanks,
James

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