Skip to content

Instantly share code, notes, and snippets.

@James-Ansley
Last active April 23, 2024 20:38
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • 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.

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