几年前买的UNAS 201P,当时硬件性价比还可以,就买了,但系统不是太好用。去年接触了飞牛系统后觉得很不错,就在另一个小电脑上测试学习。年前刚刚把UNAS安装了飞牛的系统,但是无法直接使用飞牛系统控制风扇,所以才弄了这个脚本。当然也都是基于网络搜索各位大神的资料后,学习优化后改为可以用于UNAS的脚本。
需要新建两个文件
1.fan_control.sh 具体控制风扇的程序
2.fan_control.service 控制定时执行脚本的服务
在这之前可能需要安装基础依赖:
- hddtemp 测试硬盘温度
- lm-sensors 测试CPU温度
- i2c-tools 写入风扇数值
- hdparm 检测硬盘是否休眠
- smartmontools 检测硬盘smart信息
apt update && apt install -y hdparm smartmontools lm-sensors i2c-tools hddtemp
sensors-detect # 按提示完成传感器检测(一路按回车即可)
sudo sensors-detect --auto #这个命令会自动检测你的CPU温度传感器并加载驱动,全程自动无需手动操作
注:hddtemp 在 Debian/Ubuntu 22.04 及以后(包括 fnOS/Debian bookworm)已被官方仓库移除,所以 apt install hddtemp 会报“Package ‘hddtemp’ has no installation candidate”。
仍想用原版 hddtemp,可手动装旧 deb Ubuntu 20.04 的最后一个构建在 22.04/Debian12 上依旧能跑,依赖无变化:
wget http://archive.ubuntu.com/ubuntu/pool/universe/h/hddtemp/hddtemp\_0.3-beta15-53\_amd64.deb
sudo apt install ./hddtemp\_0.3-beta15-53\_amd64.deb
装完即可sudo hddtemp /dev/sd?测试是否安装成功
若以后不再需要,用 sudo apt remove hddtemp 即可干净卸载 。
安装完依赖后就是具体脚本内容:
vim /usr/local/bin/fan_control.sh,使用命令新建sh文件后,把脚本内容拷贝进去
#!/bin/bash
#PIDFILE=/run/fan_control.pid
#echo $$ > "$PIDFILE"
#Bash(以及兼容的 shell)里常见的“严格模式”起手式,它把两条选项一次打开,让脚本在出错时立刻停止,防止“带病继续跑”。
set -eo pipefail
# ================================== 配置项 ==================================
STATE_FILE="/var/run/fan_control.state" # 持久化存储当前状态
LOG_FILE="/var/log/fan_control.log" # 日志文件
FAN_CTRL_BIN="/usr/sbin/i2cset" # 风扇设置命令
FAN_CTRL=(/usr/sbin/i2cset -y 0 0x54 0xf0) # 风扇设置命令带参数
CHECK_INTERVAL=60 # 检测间隔(秒)
SAFE_CHECKS_MAX=3 # 连续n次检测正常才降速后降低风扇速度
MAX_CPU_TEMP=65 # 降低CPU阈值,更保守
MAX_HDD_TEMP=45 # 降低硬盘阈值,保护数据
SAFE_FAN_SPEED=25 # 降低默认转速,更安静
INCREASE_STEP=5 # 减小步进,更平滑
DECREASE_STEP=5 # 新增降温步进
MAX_FAN_SPEED=100 # 最高转速
MIN_FAN_SPEED=20 # 最低转速
LO**AX_SIZE=10485760 # 日志最大10MB
# 颜色定义(避免未定义导致的输出异常)
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# =============================================================================
# 初始化日志和状态文件
init_files() {
# 创建日志文件(确保权限)
if [ ! -f "$LOG_FILE" ]; then
touch "$LOG_FILE" 2>/dev/null || {
echo "错误:无法创建日志文件 $LOG_FILE"
exit 1
}
chmod 644 "$LOG_FILE"
fi
# 日志轮转检查
if [ -f "$LOG_FILE" ]; then
local log_size=$(stat -f%z "$LOG_FILE" 2>/dev/null || stat -c%s "$LOG_FILE" 2>/dev/null)
if [ "$log_size" -gt "$LO**AX_SIZE" ]; then
mv "$LOG_FILE" "${LOG_FILE}.old"
touch "$LOG_FILE"
log "日志文件已轮转(大小超过 ${LO**AX_SIZE} 字节)"
fi
fi
# 确保状态文件目录存在(/var/run可能被清空)
mkdir -p "$(dirname "$STATE_FILE")"
if [ ! -f "$STATE_FILE" ]; then
# 使用原子写入,避免文件内容不完整
{
echo "current_speed=$SAFE_FAN_SPEED"
echo "last_action=initialized"
echo "last_temp_check=$(date +%s)"
echo "consecutive_safe_checks=0"
} > "$STATE_FILE"
chmod 644 "$STATE_FILE"
# 初始化风扇转速
if control_fan "$SAFE_FAN_SPEED" >/dev/null 2>&1; then
log "初始化風扇轉速為 $SAFE_FAN_SPEED%"
else
log "警告:初始化風扇轉速失敗 (退出碼 $?)"
fi
fi
}
log() {
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
echo "[$timestamp] $1" >> "$LOG_FILE" 2>/dev/null
}
read_state() {
# 先检查文件是否存在,不存在则初始化
if [ ! -f "$STATE_FILE" ]; then
init_files
fi
# 安全读取状态文件,避免单个变量错误导致整体失败
# 逐个读取变量,而不是直接source整个文件
current_speed=$(grep -E '^current_speed=' "$STATE_FILE" | cut -d'=' -f2 | head -1)
last_action=$(grep -E '^last_action=' "$STATE_FILE" | cut -d'=' -f2- | head -1)
last_temp_check=$(grep -E '^last_temp_check=' "$STATE_FILE" | cut -d'=' -f2 | head -1)
consecutive_safe_checks=$(grep -E '^consecutive_safe_checks=' "$STATE_FILE" | cut -d'=' -f2 | head -1)
# 变量默认值兜底
current_speed=${current_speed:-$SAFE_FAN_SPEED}
last_action=${last_action:-"unknown"}
last_temp_check=${last_temp_check:-$(date +%s)}
consecutive_safe_checks=${consecutive_safe_checks:-0}
# 验证变量有效性,无效则重置
if ! [[ "$current_speed" =~ ^[0-9]+$ ]]; then
current_speed=$SAFE_FAN_SPEED
fi
if ! [[ "$consecutive_safe_checks" =~ ^[0-9]+$ ]]; then
consecutive_safe_checks=0
fi
}
update_state() {
local speed=$1
local action=$2
local safe_checks=${3:-0}
# 先记录日志(避免日志污染状态文件)
# log "更新状态 - 转速: $speed | 操作: $action | 连续安全检测: $safe_checks"
# 使用临时文件原子写入,避免写入过程中文件损坏
local temp_file=$(mktemp /tmp/fan_control.XXXXXX)
{
echo "current_speed=$speed"
echo "last_action=$action"
echo "last_temp_check=$(date +%s)"
echo "consecutive_safe_checks=$safe_checks"
} > "$temp_file"
# 原子替换状态文件,避免读取时文件内容不完整
mv "$temp_file" "$STATE_FILE" || {
log "警告:更新状态文件失败"
rm -f "$temp_file"
}
}
# 优化的CPU温度获取(多种方法,增加容错)
get_cpu_temp() {
local temp=""
# 方法1: sensors命令(需要lm-sensors)获取多核的最高温度
if [ -z "$temp" ] && command -v sensors >/dev/null 2>&1; then
raw_temps=( $(sensors 2>/dev/null | awk '/^Core [0-9]:/ {gsub(/\+|°C/,"",$3); print $3}') )
# 最高温度
temp=$(printf '%s\n' "${raw_temps[@]}" | sort -nr | head -n1)
# log "cpu(sensors): $temp"
fi
# 方法2: thermal_zone(最常见)
if [ -z "$temp" ]; then
raw_temps=( $(cat /sys/class/thermal/thermal_zone*/temp 2>/dev/null) )
# 最高温度
max_temp=$(printf '%s\n' "${raw_temps[@]}" | sort -nr | head -n1)
if [ -n "$max_temp" ] && [ "$max_temp" -gt 1000 ]; then
temp=$((max_temp / 1000))
fi
# log "cpu(thermal): $temp"
fi
# 方法3: hwmon(适用于某些主板)
if [ -z "$temp" ]; then
local hwmon_temp=$(find /sys/class/hwmon -name "temp*_input" 2>/dev/null | head -1)
if [ -n "$hwmon_temp" ]; then
local raw_temp=$(cat "$hwmon_temp" 2>/dev/null)
if [ -n "$raw_temp" ] && [ "$raw_temp" -gt 1000 ]; then
temp=$((raw_temp / 1000))
#log "cpu(hwmon): $temp"
fi
fi
fi
# 方法4: 尝试ACPI
if [ -z "$temp" ] && command -v acpi >/dev/null 2>&1; then
temp=$(acpi -t 2>/dev/null | grep -oP '\d+\.\d+' | cut -d. -f1 | head -1)
#log "cpu(acpi): $temp"
fi
# 方法5:尝试proc文件系统
if [ -z "$temp" ]; then
local proc_temp=$(cat /proc/cpuinfo 2>/dev/null | grep -i 'temperature' | grep -oP '\d+' | head -1)
if [ -n "$proc_temp" ]; then
temp=$proc_temp
#log "cpu(proc): $proc_temp"
fi
fi
temp_int=${temp%.*} # 去掉小数点及后面的内容,即取整
if [ -n "$temp_int" ] && [ "$temp_int" -gt 0 ] && [ "$temp_int" -lt 150 ]; then
echo "$temp_int"
return 0
fi
log "警告:所有方法失败,无法获取CPU温度"
return 1
}
# 优化的硬盘温度获取
get_hdd_temp() {
local max_hdd_temp=0
local hdd_count=0
# 获取所有硬盘设备(排除回环设备和分区)
local hdd_devices=$(lsblk -d -o NAME,TYPE 2>/dev/null | awk '$2=="disk" && $1!~/loop/ {print $1}')
for dev in $hdd_devices; do
local device="/dev/$dev"
# 检查设备是否存在
[ -b "$device" ] || continue
# 检查是否休眠(避免唤醒)
local power_state=$(hdparm -C "$device" 2>/dev/null | grep -i 'drive state')
if echo "$power_state" | grep -qiE 'standby|sleeping'; then
log "$dev 已休眠,跳过温度检测"
continue
fi
local temp=""
# 优先使用smartctl
if command -v smartctl >/dev/null 2>&1; then
temp=$(smartctl -A "$device" 2>/dev/null | \
grep -iE '194|Temperature_Celsius' | \
awk '{print $10}' | head -1)
# log "$dev 温度(smartctl): $temp"
fi
# 备用hddtemp
if [ -z "$temp" ] && command -v hddtemp >/dev/null 2>&1; then
temp=$(hddtemp -n "$device" 2>/dev/null)
# log "$dev 温度(hddtemp): $temp"
fi
# 验证温度有效性
if [ -n "$temp" ] && [ "$temp" -gt 0 ] && [ "$temp" -lt 100 ]; then
hdd_count=$((hdd_count + 1))
if [ "$temp" -gt "$max_hdd_temp" ]; then
max_hdd_temp=$temp
fi
fi
done
if [ "$max_hdd_temp" -gt 0 ]; then
echo "$max_hdd_temp"
return 0
fi
return 1
}
control_fan() {
# 判断命令可用
if [ ! -x "$FAN_CTRL_BIN" ]; then
log "错误:未找到风扇控制程序或不可执行 $FAN_CTRL_BIN"
return 1
fi
local target_speed=$1
# 限制转速范围
if [ "$target_speed" -lt "$MIN_FAN_SPEED" ]; then
target_speed=$MIN_FAN_SPEED
elif [ "$target_speed" -gt "$MAX_FAN_SPEED" ]; then
target_speed=$MAX_FAN_SPEED
fi
# 执行风速设置
if eval "${FAN_CTRL[@]}" "$target_speed" >/dev/null 2>&1; then
return 0
else
log "错误:设置风扇转速 $target_speed% 失败 (退出码: $?)"
return 1
fi
}
# 优化的温度监控(更平滑的调速策略)
monitor_temps() {
read_state
local current_speed=${current_speed:-$SAFE_FAN_SPEED}
local safe_checks=${consecutive_safe_checks:-0}
local cpu_temp=$(get_cpu_temp)
local hdd_temp=$(get_hdd_temp)
# 至少需要一个有效温度
if [ -z "$cpu_temp" ] && [ -z "$hdd_temp" ]; then
log "警告:无法获取任何温度数据,保持当前转速"
return
fi
# 计算温度超标程度
local cpu_overheat=0
local hdd_overheat=0
if [ -n "$cpu_temp" ]; then
cpu_overheat=$((cpu_temp - MAX_CPU_TEMP))
[ $cpu_overheat -lt 0 ] && cpu_overheat=0
fi
if [ -n "$hdd_temp" ]; then
hdd_overheat=$((hdd_temp - MAX_HDD_TEMP))
[ $hdd_overheat -lt 0 ] && hdd_overheat=0
fi
local max_overheat=$cpu_overheat
[ $hdd_overheat -gt $max_overheat ] && max_overheat=$hdd_overheat
local target_speed=$current_speed
local action="温度正常"
if [ $max_overheat -gt 0 ]; then
# 超温:根据超标程度调整增幅
safe_checks=0
if [ $max_overheat -ge 10 ]; then
# 严重超温:快速增加
target_speed=$((current_speed + INCREASE_STEP * 3))
action="严重超温!快速提升转速"
elif [ $max_overheat -ge 5 ]; then
# 中度超温
target_speed=$((current_speed + INCREASE_STEP * 2))
action="中度超温,提升转速"
else
# 轻度超温
target_speed=$((current_speed + INCREASE_STEP))
action="轻度超温,微调转速"
fi
else
# 温度正常:逐步降低转速(避免频繁波动)
safe_checks=$((safe_checks + 1))
if [ $safe_checks -ge $SAFE_CHECKS_MAX ] && [ $current_speed -gt $SAFE_FAN_SPEED ]; then
# 连续n次检测正常才降速
target_speed=$((current_speed - DECREASE_STEP))
[ $target_speed -lt $SAFE_FAN_SPEED ] && target_speed=$SAFE_FAN_SPEED
action="温度持续正常,降低转速"
safe_checks=0
else
# 根据检测间隔*次数计算温度正常持续时间
total_time=$(convert_seconds $((CHECK_INTERVAL * safe_checks)))
if [ $safe_checks -gt $SAFE_CHECKS_MAX ]; then
action="温度正常(${total_time})"
else
action="温度正常(${safe_checks}/$SAFE_CHECKS_MAX)"
fi
fi
fi
# 执行调整
if [ $target_speed -ne $current_speed ]; then
if control_fan "$target_speed"; then
log "⚙️ $action | CPU: ${cpu_temp:-N/A}°C | HDD: ${hdd_temp:-N/A}°C | 转速: ${current_speed}% → ${target_speed}%"
update_state "$target_speed" "$action" "$safe_checks"
fi
else
log "✓ CPU: ${cpu_temp:-N/A}°C | HDD: ${hdd_temp:-N/A}°C | 转速: ${current_speed}% | ${action}"
update_state "$target_speed" "$action" "$safe_checks"
fi
}
show_status() {
read_state
local cpu_temp=$(get_cpu_temp)
local hdd_temp=$(get_hdd_temp)
echo -e "${GREEN}===== 飞牛NAS风扇温控状态 =====${NC}"
# echo "from state:consecutive_safe_checks=${consecutive_safe_checks}"
echo -e "当前风扇转速: ${YELLOW}${current_speed}%${NC}"
# 判断CPU温度值是否有值
if [ -n "$cpu_temp" ]; then
echo -e "CPU温度: ${cpu_temp}°C (阈值: ${MAX_CPU_TEMP}°C)"
else
echo -e "CPU温度: 未检测到"
fi
# 判断硬盘温度值是否有值
if [ -n "$hdd_temp" ]; then
echo -e "硬盘温度: ${hdd_temp}°C (阈值: ${MAX_HDD_TEMP}°C)"
else
echo -e "硬盘温度: 未检测到或者硬盘均休眠"
fi
echo ""
echo "最后操作: $last_action"
echo "检测间隔: ${CHECK_INTERVAL}秒"
echo "转速范围: ${MIN_FAN_SPEED}%-${MAX_FAN_SPEED}%"
echo -e "${GREEN}==============================${NC}"
}
# 手动设置转速
set_speed() {
local speed=$1
if [ -z "$speed" ]; then
echo "用法: $0 --set-speed <20-100>"
exit 1
fi
if control_fan "$speed"; then
update_state "$speed" "手动设置为 ${speed}%" 0
echo -e "${GREEN}✓ 风扇转速已设置为 ${speed}%${NC}"
else
echo -e "${RED}✗ 设置失败${NC}"
exit 1
fi
}
# 秒数转人性化时长:x年x月x天x小时x分钟x秒
convert_seconds() {
# 检查传入参数是否为正整数
if [ $# -ne 1 ] || ! [[ $1 =~ ^[0-9]+$ ]]; then
return 1
fi
# 定义时间换算基准(日常通用标准,可根据需求修改)
local SECONDS=$1
local SEC_PER_MIN=60
local SEC_PER_HOUR=$((SEC_PER_MIN * 60))
local SEC_PER_DAY=$((SEC_PER_HOUR * 24))
local SEC_PER_MONTH=$((SEC_PER_DAY * 30)) # 按30天/月
local SEC_PER_YEAR=$((SEC_PER_DAY * 365)) # 按365天/年
# 计算各单位的数值,取整后更新剩余秒数
local year=$((SECONDS / SEC_PER_YEAR))
local remain=$((SECONDS % SEC_PER_YEAR))
local month=$((remain / SEC_PER_MONTH))
remain=$((remain % SEC_PER_MONTH))
local day=$((remain / SEC_PER_DAY))
remain=$((remain % SEC_PER_DAY))
local hour=$((remain / SEC_PER_HOUR))
remain=$((remain % SEC_PER_HOUR))
local min=$((remain / SEC_PER_MIN))
local sec=$((remain % SEC_PER_MIN))
# 拼接结果(仅保留有值的单位,避免0年0月等无效显示)
local result=""
[ $year -gt 0 ] && result+="${year}年"
[ $month -gt 0 ] && result+="${month}月"
[ $day -gt 0 ] && result+="${day}天"
[ $hour -gt 0 ] && result+="${hour}小时"
[ $min -gt 0 ] && result+="${min}分钟"
# 最后保留秒,即使秒数为0(比如刚好1分钟,显示1分钟0秒)
result+="${sec}秒"
# 输出最终结果
echo "${result}"
return 0
}
main() {
# 必须使用root运行
if [ "$(id -u)" -ne 0 ]; then
echo -e "${RED}错误:请使用 sudo 运行${NC}"
exit 1
fi
# 加载i2c工具
modprobe i2c-dev >/dev/null 2>&1 || log "警告:加载i2c-dev模块失败"
init_files
case "$1" in
--monitor)
log "========== 执行单次风扇温控检测 =========="
echo -e "${GREEN}执行单次风扇温控检测${NC}"
monitor_temps
exit 0
;;
--service)
log "========== 风扇温控服务启动 =========="
echo -e "${GREEN}风扇温控服务已启动(间隔: ${CHECK_INTERVAL}秒)${NC}"
echo "使用 Ctrl+C 停止,或运行: sudo $0 --status 查看状态"
( while true; do
monitor_temps
sleep $CHECK_INTERVAL
done ) &
exit 0
;;
--status)
show_status
;;
--log)
if [ -f "$LOG_FILE" ]; then
tail -n 50 -f "$LOG_FILE"
else
echo "日志文件不存在"
fi
;;
--set-speed)
set_speed "$2"
;;
--restart)
log "手动重启服务"
control_fan "$SAFE_FAN_SPEED"
update_state "$SAFE_FAN_SPEED" "手动重启" 0
echo -e "${GREEN}✓ 服务已重启,转速重置为 ${SAFE_FAN_SPEED}%${NC}"
;;
--test)
echo "执行温度检测测试..."
local cpu=$(get_cpu_temp || echo "未检测到")
local hdd=$(get_hdd_temp || echo "未检测到(可能硬盘均休眠)")
echo "CPU温度: $cpu°C"
echo "硬盘温度: $hdd°C"
;;
*)
echo "飞牛NAS风扇温控脚本"
echo ""
echo "用法:"
echo " sudo $0 --monitor 执行单次温控检测"
echo " sudo $0 --service 启动温控服务"
echo " sudo $0 --status 查看当前状态"
echo " sudo $0 --log 查看实时日志"
echo " sudo $0 --set-speed <值> 设置风扇转速(20-100)"
echo " sudo $0 --restart 重启服务并重置风扇转速"
echo " sudo $0 --test 测试温度检测"
exit 1
;;
esac
}
main "$@"
新建service文件
vim /etc/systemd/system/fan_control.service
内容如下:
[Unit]
Description=飞牛风扇控制服务
[Service]
Type=simple
ExecStartPre=/bin/sleep 60
ExecStart=/usr/local/bin/fan_control.sh --monitor
Restart=always
RestartSec=60
User=root
WorkingDirectory=/root
StandardOutput=append:/var/log/fan_control.log
StandardError=append:/var/log/fan_control.log
[Install]
WantedBy=multi-user.target
使用命令启用服务和设置开机启动
# 重新加载 systemd 配置(必须执行)
sudo systemctl daemon-reload
#设置开机启动
sudo systemctl enable fan_control.service
# 检查状态(最重要!)
sudo systemctl status fan_control.service # 第一次执行后的状态
# 查看日志
sudo journalctl -u fan_control.service -f # 实时跟踪
# 或直接看你脚本里的日志文件
tail -f /var/log/fan_control.log
也可使用sh脚本指令查看状态信息等:
执行单次检测
/usr/local/bin/fan\_control.sh --monitor
查看状态(验证是否正常运行)
/usr/local/bin/fan\_control.sh --status
查看log
/usr/local/bin/fan\_control.sh --log
设置风扇转速(20-100)
/usr/local/bin/fan\_control.sh --set-speed <值>