mirror of
https://github.com/lkddi/dell-fans-controller-docker.git
synced 2026-05-18 21:57:29 +08:00
支持通过环境变量配置温控档位
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
HOST=192.168.1.100
|
HOST=192.168.1.100
|
||||||
USERNAME=root
|
USERNAME=root
|
||||||
PASSWORD=your_idrac_password
|
PASSWORD=your_idrac_password
|
||||||
|
FAN_SPEED_STEPS=50:20,55:25,60:30,65:40
|
||||||
|
|||||||
+2
-1
@@ -3,12 +3,13 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
- 准备公开开源发布流程,Docker Hub 镜像统一为 `lkddi/dell-fans-controller`。
|
- 准备公开开源发布流程,Docker Hub 镜像统一为 `lkddi/dell-fans-controller`。
|
||||||
- GitHub Actions 改为 PR/master 构建验证,`v*` tag 才发布 Docker 镜像。
|
- GitHub Actions 改为 PR 构建验证,`v*` tag 才发布 Docker 镜像。
|
||||||
- 新增 Docker、Docker Compose 和本机 Python 三种运行说明。
|
- 新增 Docker、Docker Compose 和本机 Python 三种运行说明。
|
||||||
- 移除代码中的默认 iDRAC 地址、账号和密码,改为必须通过环境变量配置。
|
- 移除代码中的默认 iDRAC 地址、账号和密码,改为必须通过环境变量配置。
|
||||||
- 增加 `.env.example`、`.dockerignore` 和 MIT License。
|
- 增加 `.env.example`、`.dockerignore` 和 MIT License。
|
||||||
- 清理 Python 缓存文件,避免将运行产物提交到仓库。
|
- 清理 Python 缓存文件,避免将运行产物提交到仓库。
|
||||||
- Docker 运行镜像切换到 Debian slim,只安装 `python3`、`ipmitool` 和时区数据,在降低体积的同时保留更好的IPMI兼容性。
|
- Docker 运行镜像切换到 Debian slim,只安装 `python3`、`ipmitool` 和时区数据,在降低体积的同时保留更好的IPMI兼容性。
|
||||||
|
- 新增 `FAN_SPEED_STEPS` 环境变量,允许用户通过 `.env` 自定义温度阈值和风扇转速档位。
|
||||||
|
|
||||||
## Previous Improvements
|
## Previous Improvements
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ docker run -d --name dell-fans-controller \
|
|||||||
-e HOST=192.168.1.100 \
|
-e HOST=192.168.1.100 \
|
||||||
-e USERNAME=root \
|
-e USERNAME=root \
|
||||||
-e PASSWORD=your_idrac_password \
|
-e PASSWORD=your_idrac_password \
|
||||||
|
-e FAN_SPEED_STEPS=50:20,55:25,60:30,65:40 \
|
||||||
lkddi/dell-fans-controller:latest
|
lkddi/dell-fans-controller:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ cp .env.example .env
|
|||||||
HOST=192.168.1.100
|
HOST=192.168.1.100
|
||||||
USERNAME=root
|
USERNAME=root
|
||||||
PASSWORD=your_idrac_password
|
PASSWORD=your_idrac_password
|
||||||
|
FAN_SPEED_STEPS=50:20,55:25,60:30,65:40
|
||||||
```
|
```
|
||||||
|
|
||||||
Start the service / 启动服务:
|
Start the service / 启动服务:
|
||||||
@@ -88,6 +90,7 @@ Run / 运行:
|
|||||||
export HOST=192.168.1.100
|
export HOST=192.168.1.100
|
||||||
export USERNAME=root
|
export USERNAME=root
|
||||||
export PASSWORD=your_idrac_password
|
export PASSWORD=your_idrac_password
|
||||||
|
export FAN_SPEED_STEPS=50:20,55:25,60:30,65:40
|
||||||
python3 start.py
|
python3 start.py
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -98,6 +101,7 @@ python3 start.py
|
|||||||
| `HOST` | Yes | iDRAC management IP address. / iDRAC 管理 IP。 |
|
| `HOST` | Yes | iDRAC management IP address. / iDRAC 管理 IP。 |
|
||||||
| `USERNAME` | Yes | iDRAC username with IPMI permission. / 有 IPMI 权限的 iDRAC 用户名。 |
|
| `USERNAME` | Yes | iDRAC username with IPMI permission. / 有 IPMI 权限的 iDRAC 用户名。 |
|
||||||
| `PASSWORD` | Yes | iDRAC password. / 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.
|
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%` |
|
| `60-65 C` | `40%` |
|
||||||
| `>65 C` | iDRAC automatic mode / iDRAC 自动模式 |
|
| `>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 / 故障排查
|
## Troubleshooting / 故障排查
|
||||||
|
|
||||||
Test IPMI manually first / 先手动测试 IPMI:
|
Test IPMI manually first / 先手动测试 IPMI:
|
||||||
@@ -135,9 +163,9 @@ Common issues / 常见问题:
|
|||||||
|
|
||||||
## Release / 发布新版本
|
## 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
|
```bash
|
||||||
git tag v1.0.0
|
git tag v1.0.0
|
||||||
|
|||||||
+61
-11
@@ -3,19 +3,74 @@ from controller.logger import logger
|
|||||||
from controller.ipmi import IpmiTool
|
from controller.ipmi import IpmiTool
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_FAN_SPEED_STEPS = (
|
||||||
|
(50, 20),
|
||||||
|
(55, 25),
|
||||||
|
(60, 30),
|
||||||
|
(65, 40),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# 风扇控制器:根据iDRAC温度传感器结果自动切换风扇模式和转速
|
# 风扇控制器:根据iDRAC温度传感器结果自动切换风扇模式和转速
|
||||||
class FanController:
|
class FanController:
|
||||||
|
|
||||||
# 初始化控制器并记录iDRAC连接信息
|
# 初始化控制器并记录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.host = host
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
|
|
||||||
self.ipmi = IpmiTool(self.host, self.username, self.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.last_set_speed = None # 记录最后设置的风扇速度
|
||||||
self.is_auto_mode = False # 记录当前是否为自动模式
|
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):
|
def set_fan_speed(self, speed: int):
|
||||||
logger.info(f'设置风扇速度: {speed}%')
|
logger.info(f'设置风扇速度: {speed}%')
|
||||||
@@ -28,16 +83,11 @@ class FanController:
|
|||||||
:param temperature: 当前最高温度
|
:param temperature: 当前最高温度
|
||||||
:return: 对应的风扇转速百分比,如果应该切换到自动模式则返回-1
|
:return: 对应的风扇转速百分比,如果应该切换到自动模式则返回-1
|
||||||
"""
|
"""
|
||||||
if 0 < temperature <= 50:
|
for threshold, speed in self.fan_speed_steps:
|
||||||
return 20
|
if 0 < temperature <= threshold:
|
||||||
elif 50 < temperature <= 55:
|
return speed
|
||||||
return 25
|
|
||||||
elif 55 < temperature <= 60:
|
return -1 # 表示应切换到自动模式
|
||||||
return 30
|
|
||||||
elif 60 < temperature <= 65:
|
|
||||||
return 40
|
|
||||||
else:
|
|
||||||
return -1 # 表示应切换到自动模式
|
|
||||||
|
|
||||||
# 执行一次完整的温度读取和风扇控制周期
|
# 执行一次完整的温度读取和风扇控制周期
|
||||||
def run(self):
|
def run(self):
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ if __name__ == '__main__':
|
|||||||
host = os.getenv('HOST')
|
host = os.getenv('HOST')
|
||||||
username = os.getenv('USERNAME')
|
username = os.getenv('USERNAME')
|
||||||
password = os.getenv('PASSWORD')
|
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:
|
if not host:
|
||||||
raise RuntimeError('未设置 HOST 环境变量')
|
raise RuntimeError('未设置 HOST 环境变量')
|
||||||
|
|
||||||
@@ -21,7 +25,12 @@ if __name__ == '__main__':
|
|||||||
raise RuntimeError('未设置 PASSWORD 环境变量')
|
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:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user