支持通过环境变量配置温控档位

This commit is contained in:
2026-05-06 10:04:15 +08:00
parent 929652bc00
commit eabc8d8bd2
6 changed files with 158 additions and 15 deletions
+1
View File
@@ -1,3 +1,4 @@
HOST=192.168.1.100
USERNAME=root
PASSWORD=your_idrac_password
FAN_SPEED_STEPS=50:20,55:25,60:30,65:40
+2 -1
View File
@@ -3,12 +3,13 @@
## Unreleased
- 准备公开开源发布流程,Docker Hub 镜像统一为 `lkddi/dell-fans-controller`
- GitHub Actions 改为 PR/master 构建验证,`v*` tag 才发布 Docker 镜像。
- GitHub Actions 改为 PR 构建验证,`v*` tag 才发布 Docker 镜像。
- 新增 Docker、Docker Compose 和本机 Python 三种运行说明。
- 移除代码中的默认 iDRAC 地址、账号和密码,改为必须通过环境变量配置。
- 增加 `.env.example``.dockerignore` 和 MIT License。
- 清理 Python 缓存文件,避免将运行产物提交到仓库。
- Docker 运行镜像切换到 Debian slim,只安装 `python3``ipmitool` 和时区数据,在降低体积的同时保留更好的IPMI兼容性。
- 新增 `FAN_SPEED_STEPS` 环境变量,允许用户通过 `.env` 自定义温度阈值和风扇转速档位。
## Previous Improvements
+30 -2
View File
@@ -36,6 +36,7 @@ docker run -d --name dell-fans-controller \
-e HOST=192.168.1.100 \
-e USERNAME=root \
-e PASSWORD=your_idrac_password \
-e FAN_SPEED_STEPS=50:20,55:25,60:30,65:40 \
lkddi/dell-fans-controller:latest
```
@@ -61,6 +62,7 @@ cp .env.example .env
HOST=192.168.1.100
USERNAME=root
PASSWORD=your_idrac_password
FAN_SPEED_STEPS=50:20,55:25,60:30,65:40
```
Start the service / 启动服务:
@@ -88,6 +90,7 @@ Run / 运行:
export HOST=192.168.1.100
export USERNAME=root
export PASSWORD=your_idrac_password
export FAN_SPEED_STEPS=50:20,55:25,60:30,65:40
python3 start.py
```
@@ -98,6 +101,7 @@ python3 start.py
| `HOST` | Yes | iDRAC management IP address. / iDRAC 管理 IP。 |
| `USERNAME` | Yes | iDRAC username with IPMI permission. / 有 IPMI 权限的 iDRAC 用户名。 |
| `PASSWORD` | Yes | iDRAC password. / iDRAC 密码。 |
| `FAN_SPEED_STEPS` | No | Temperature-to-speed rules. Default: `50:20,55:25,60:30,65:40`. / 温度和风扇转速规则,默认值:`50:20,55:25,60:30,65:40`。 |
The application does not include default credentials. Missing variables will stop startup with a clear error.
@@ -117,6 +121,30 @@ The controller reads all temperature sensors and uses the highest value.
| `60-65 C` | `40%` |
| `>65 C` | iDRAC automatic mode / iDRAC 自动模式 |
Customize the policy with `FAN_SPEED_STEPS`. Each item uses `temperature:speed`, separated by commas. The controller sorts thresholds from low to high. When the current highest temperature is greater than the last threshold, it switches to iDRAC automatic mode.
可以通过 `FAN_SPEED_STEPS` 自定义温控策略。每一项格式为 `温度:转速`,多项用英文逗号分隔。控制器会按温度阈值从低到高排序;当最高温度超过最后一个阈值时,自动切回 iDRAC 自动模式。
Invalid values stop startup with a clear error. Fan speed must be an integer from `10` to `100`, and temperature thresholds cannot be duplicated.
配置错误会在启动时直接报错退出。风扇转速必须是 `10``100` 的整数,温度阈值不能重复。
Example / 示例:
```env
FAN_SPEED_STEPS=45:20,55:30,62:45,70:60
```
This means / 含义:
| Temperature / 温度 | Fan Speed / 风扇转速 |
| --- | --- |
| `0-45 C` | `20%` |
| `45-55 C` | `30%` |
| `55-62 C` | `45%` |
| `62-70 C` | `60%` |
| `>70 C` | iDRAC automatic mode / iDRAC 自动模式 |
## Troubleshooting / 故障排查
Test IPMI manually first / 先手动测试 IPMI
@@ -135,9 +163,9 @@ Common issues / 常见问题:
## Release / 发布新版本
Docker images are published only from Git tags matching `v*`. Pull requests and `master` pushes only build and verify the image.
Docker images are published only from Git tags matching `v*`. Pull requests build and verify the image, while direct `master` pushes do not trigger Docker builds.
Docker 镜像只在推送 `v*` tag 时发布。PR `master` 推送只做构建验证,不覆盖 `latest`
Docker 镜像只在推送 `v*` tag 时发布。PR 会做构建验证,直接推送 `master` 不触发 Docker 构建
```bash
git tag v1.0.0
+60 -10
View File
@@ -3,19 +3,74 @@ from controller.logger import logger
from controller.ipmi import IpmiTool
DEFAULT_FAN_SPEED_STEPS = (
(50, 20),
(55, 25),
(60, 30),
(65, 40),
)
# 风扇控制器:根据iDRAC温度传感器结果自动切换风扇模式和转速
class FanController:
# 初始化控制器并记录iDRAC连接信息
def __init__(self, host: str, username: str, password: str):
def __init__(self, host: str, username: str, password: str, fan_speed_steps: str = None):
self.host = host
self.username = username
self.password = password
self.ipmi = IpmiTool(self.host, self.username, self.password)
self.fan_speed_steps = self.parse_fan_speed_steps(fan_speed_steps)
self.last_set_speed = None # 记录最后设置的风扇速度
self.is_auto_mode = False # 记录当前是否为自动模式
# 解析温控规则配置,格式为 "50:20,55:25,60:30,65:40"
def parse_fan_speed_steps(self, steps: str) -> tuple:
"""
解析环境变量中的温控规则
:param steps: 温度阈值和风扇转速配置
:return: 按温度升序排列的规则元组
"""
if steps is None:
return DEFAULT_FAN_SPEED_STEPS
if not steps.strip():
raise ValueError('FAN_SPEED_STEPS 至少需要包含一条温控规则')
parsed_rules = []
for item in steps.split(','):
item = item.strip()
if not item:
continue
try:
temperature_text, speed_text = item.split(':', 1)
temperature = int(temperature_text.strip())
speed = int(speed_text.strip())
except ValueError as exc:
raise ValueError(
f'FAN_SPEED_STEPS 格式错误: {steps},正确示例: 50:20,55:25,60:30,65:40'
) from exc
if temperature <= 0:
raise ValueError('FAN_SPEED_STEPS 温度阈值必须大于0')
if speed < 10 or speed > 100:
raise ValueError('FAN_SPEED_STEPS 风扇转速必须在10到100之间')
parsed_rules.append((temperature, speed))
if not parsed_rules:
raise ValueError('FAN_SPEED_STEPS 至少需要包含一条温控规则')
parsed_rules.sort(key=lambda rule: rule[0])
temperatures = [rule[0] for rule in parsed_rules]
if len(temperatures) != len(set(temperatures)):
raise ValueError('FAN_SPEED_STEPS 不能包含重复的温度阈值')
return tuple(parsed_rules)
# 设置手动风扇速度
def set_fan_speed(self, speed: int):
logger.info(f'设置风扇速度: {speed}%')
@@ -28,15 +83,10 @@ class FanController:
:param temperature: 当前最高温度
:return: 对应的风扇转速百分比,如果应该切换到自动模式则返回-1
"""
if 0 < temperature <= 50:
return 20
elif 50 < temperature <= 55:
return 25
elif 55 < temperature <= 60:
return 30
elif 60 < temperature <= 65:
return 40
else:
for threshold, speed in self.fan_speed_steps:
if 0 < temperature <= threshold:
return speed
return -1 # 表示应切换到自动模式
# 执行一次完整的温度读取和风扇控制周期
+10 -1
View File
@@ -11,6 +11,10 @@ if __name__ == '__main__':
host = os.getenv('HOST')
username = os.getenv('USERNAME')
password = os.getenv('PASSWORD')
# 优先读取新的温控档位变量,并兼容旧别名
fan_speed_steps = os.getenv('FAN_SPEED_STEPS')
if fan_speed_steps is None:
fan_speed_steps = os.getenv('FAN_SPEED_RULES')
if not host:
raise RuntimeError('未设置 HOST 环境变量')
@@ -21,7 +25,12 @@ if __name__ == '__main__':
raise RuntimeError('未设置 PASSWORD 环境变量')
# 复用控制器实例,避免每轮循环丢失上次设置状态
client = FanController(host=host, username=username, password=password)
client = FanController(
host=host,
username=username,
password=password,
fan_speed_steps=fan_speed_steps,
)
while True:
try:
+54
View File
@@ -0,0 +1,54 @@
import unittest
from controller.client import FanController
class FanControllerConfigTest(unittest.TestCase):
def make_controller(self, fan_speed_steps=None):
return FanController(
host='127.0.0.1',
username='root',
password='password',
fan_speed_steps=fan_speed_steps,
)
def test_default_fan_speed_steps_keep_existing_policy(self):
controller = self.make_controller()
self.assertEqual(controller.get_required_fan_speed(50), 20)
self.assertEqual(controller.get_required_fan_speed(55), 25)
self.assertEqual(controller.get_required_fan_speed(60), 30)
self.assertEqual(controller.get_required_fan_speed(65), 40)
self.assertEqual(controller.get_required_fan_speed(66), -1)
self.assertEqual(controller.get_required_fan_speed(0), -1)
def test_custom_fan_speed_steps_are_sorted_by_temperature(self):
controller = self.make_controller('70:50,45:15,55:25')
self.assertEqual(controller.get_required_fan_speed(45), 15)
self.assertEqual(controller.get_required_fan_speed(46), 25)
self.assertEqual(controller.get_required_fan_speed(55), 25)
self.assertEqual(controller.get_required_fan_speed(56), 50)
self.assertEqual(controller.get_required_fan_speed(70), 50)
self.assertEqual(controller.get_required_fan_speed(71), -1)
def test_invalid_fan_speed_steps_raise_clear_error(self):
invalid_values = [
'',
'50',
'abc:20',
'50:abc',
'-1:20',
'50:9',
'50:101',
'50:20,50:30',
]
for invalid_value in invalid_values:
with self.subTest(invalid_value=invalid_value):
with self.assertRaises(ValueError):
self.make_controller(invalid_value)
if __name__ == '__main__':
unittest.main()