fan-manager 0.6.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fan_manager-0.6.3/LICENSE +20 -0
- fan_manager-0.6.3/MANIFEST.in +1 -0
- fan_manager-0.6.3/PKG-INFO +172 -0
- fan_manager-0.6.3/README.md +156 -0
- fan_manager-0.6.3/fan_manager/__init__.py +29 -0
- fan_manager-0.6.3/fan_manager/__main__.py +8 -0
- fan_manager-0.6.3/fan_manager/fan_manager.py +287 -0
- fan_manager-0.6.3/fan_manager/fan_manager_mcp.py +284 -0
- fan_manager-0.6.3/fan_manager.egg-info/PKG-INFO +172 -0
- fan_manager-0.6.3/fan_manager.egg-info/SOURCES.txt +14 -0
- fan_manager-0.6.3/fan_manager.egg-info/dependency_links.txt +1 -0
- fan_manager-0.6.3/fan_manager.egg-info/entry_points.txt +3 -0
- fan_manager-0.6.3/fan_manager.egg-info/top_level.txt +2 -0
- fan_manager-0.6.3/pyproject.toml +31 -0
- fan_manager-0.6.3/requirements.txt +0 -0
- fan_manager-0.6.3/setup.cfg +4 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2012-2023 Audel Rouhi
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include README.md include requirements.txt recursive-include fan_manager *.py
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fan-manager
|
|
3
|
+
Version: 0.6.3
|
|
4
|
+
Summary: Manager your Dell PowerEdge Fan Speed with this handy tool and MCP Server!
|
|
5
|
+
Author-email: Audel Rouhi <knucklessg1@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
8
|
+
Classifier: License :: Public Domain
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# Fan-Manager
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+

|
|
21
|
+

|
|
22
|
+

|
|
23
|
+

|
|
24
|
+

|
|
25
|
+

|
|
26
|
+
|
|
27
|
+

|
|
28
|
+

|
|
29
|
+

|
|
30
|
+

|
|
31
|
+
|
|
32
|
+

|
|
33
|
+

|
|
34
|
+

|
|
35
|
+

|
|
36
|
+

|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
*Version: 0.6.3*
|
|
40
|
+
|
|
41
|
+
Manager your Dell PowerEdge Fan Speed with this handy tool!
|
|
42
|
+
|
|
43
|
+
MCP Server for Agentic AI! Get started with Pip or Docker as well
|
|
44
|
+
|
|
45
|
+
This repository is actively maintained - Contributions are welcome!
|
|
46
|
+
|
|
47
|
+
Contribution Opportunities:
|
|
48
|
+
- Increase support of Dell PowerEdge Devices
|
|
49
|
+
- Support Non-PowerEdge Devices
|
|
50
|
+
- Support Non-Dell Devices
|
|
51
|
+
|
|
52
|
+
<details>
|
|
53
|
+
<summary><b>Usage:</b></summary>
|
|
54
|
+
|
|
55
|
+
| Short Flag | Long Flag | Description |
|
|
56
|
+
|------------|-------------|--------------------------------------------------------|
|
|
57
|
+
| -h | --help | See usage for fan-manager |
|
|
58
|
+
| -i | --intensity | Intensity of Fan Speed - Scales Logarithmically (0-10) |
|
|
59
|
+
| -c | --cold | Minimum Temperature for Fan Speed |
|
|
60
|
+
| -w | --warm | Maximum Temperature for Fan Speed |
|
|
61
|
+
| -s | --slow | Minimum Fan Speed |
|
|
62
|
+
| -f | --fast | Maximum Fan Speed |
|
|
63
|
+
| -p | --poll-rate | Poll Rate for CPU Temperature in Seconds |
|
|
64
|
+
|
|
65
|
+
</details>
|
|
66
|
+
|
|
67
|
+
<details>
|
|
68
|
+
<summary><b>Example:</b></summary>
|
|
69
|
+
|
|
70
|
+
Python
|
|
71
|
+
```bash
|
|
72
|
+
fan-manager --intensity 5 --cold 50 --warm 80 --slow 5 --fast 100 --poll-rate 24
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Docker Compose
|
|
76
|
+
|
|
77
|
+
Fan Manager
|
|
78
|
+
```docker-compose
|
|
79
|
+
---
|
|
80
|
+
services:
|
|
81
|
+
fan-manager:
|
|
82
|
+
image: knucklessg1/fan-manager:latest
|
|
83
|
+
container_name: server_fan_speed
|
|
84
|
+
privileged: true
|
|
85
|
+
environment:
|
|
86
|
+
MODE: "fan-manager"
|
|
87
|
+
INTENSITY: ${INTENSITY}
|
|
88
|
+
COLD: ${COLD}
|
|
89
|
+
WARM: ${WARM}
|
|
90
|
+
SLOW: ${SLOW}
|
|
91
|
+
FAST: ${FAST}
|
|
92
|
+
POLL_RATE: ${POLL_RATE}
|
|
93
|
+
volumes:
|
|
94
|
+
- /dev/ipmi0:/dev/ipmi0
|
|
95
|
+
restart: unless-stopped
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Fan Manager MCP Server
|
|
99
|
+
```docker-compose
|
|
100
|
+
---
|
|
101
|
+
services:
|
|
102
|
+
fan-manager-mcp:
|
|
103
|
+
image: knucklessg1/fan-manager:latest
|
|
104
|
+
container_name: server_fan_speed
|
|
105
|
+
privileged: true
|
|
106
|
+
environment:
|
|
107
|
+
MODE: "fan-manager-mcp"
|
|
108
|
+
HOST: 0.0.0.0
|
|
109
|
+
PORT: 8030
|
|
110
|
+
TRANSPORT: "http"
|
|
111
|
+
volumes:
|
|
112
|
+
- /dev/ipmi0:/dev/ipmi0
|
|
113
|
+
restart: unless-stopped
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Docker Run
|
|
117
|
+
```bash
|
|
118
|
+
docker run -it -d knucklessg1/fan-manager:latest fan-manager
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Docker Compose
|
|
122
|
+
```bash
|
|
123
|
+
docker-compose up --build -d
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Use with AI
|
|
127
|
+
|
|
128
|
+
Configure `mcp.json`
|
|
129
|
+
|
|
130
|
+
Recommended: Store secrets in environment variables with lookup in JSON file.
|
|
131
|
+
|
|
132
|
+
For Testing Only: Plain text storage will also work, although **not** recommended.
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"mcpServers": {
|
|
137
|
+
"fan-manager": {
|
|
138
|
+
"command": "uv",
|
|
139
|
+
"args": [
|
|
140
|
+
"run",
|
|
141
|
+
"--with",
|
|
142
|
+
"fan-manager",
|
|
143
|
+
"fan-manager-mcp"
|
|
144
|
+
],
|
|
145
|
+
"timeout": 200000
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
</details>
|
|
152
|
+
|
|
153
|
+
<details>
|
|
154
|
+
<summary><b>Installation Instructions:</b></summary>
|
|
155
|
+
|
|
156
|
+
Install Python Package
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
python -m pip install fan-manager
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
</details>
|
|
163
|
+
|
|
164
|
+
<details>
|
|
165
|
+
<summary><b>Repository Owners:</b></summary>
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
<img width="100%" height="180em" src="https://github-readme-stats.vercel.app/api?username=Knucklessg1&show_icons=true&hide_border=true&&count_private=true&include_all_commits=true" />
|
|
169
|
+
|
|
170
|
+

|
|
171
|
+

|
|
172
|
+
</details>
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Fan-Manager
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
|
|
11
|
+

|
|
12
|
+

|
|
13
|
+

|
|
14
|
+

|
|
15
|
+
|
|
16
|
+

|
|
17
|
+

|
|
18
|
+

|
|
19
|
+

|
|
20
|
+

|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
*Version: 0.6.3*
|
|
24
|
+
|
|
25
|
+
Manager your Dell PowerEdge Fan Speed with this handy tool!
|
|
26
|
+
|
|
27
|
+
MCP Server for Agentic AI! Get started with Pip or Docker as well
|
|
28
|
+
|
|
29
|
+
This repository is actively maintained - Contributions are welcome!
|
|
30
|
+
|
|
31
|
+
Contribution Opportunities:
|
|
32
|
+
- Increase support of Dell PowerEdge Devices
|
|
33
|
+
- Support Non-PowerEdge Devices
|
|
34
|
+
- Support Non-Dell Devices
|
|
35
|
+
|
|
36
|
+
<details>
|
|
37
|
+
<summary><b>Usage:</b></summary>
|
|
38
|
+
|
|
39
|
+
| Short Flag | Long Flag | Description |
|
|
40
|
+
|------------|-------------|--------------------------------------------------------|
|
|
41
|
+
| -h | --help | See usage for fan-manager |
|
|
42
|
+
| -i | --intensity | Intensity of Fan Speed - Scales Logarithmically (0-10) |
|
|
43
|
+
| -c | --cold | Minimum Temperature for Fan Speed |
|
|
44
|
+
| -w | --warm | Maximum Temperature for Fan Speed |
|
|
45
|
+
| -s | --slow | Minimum Fan Speed |
|
|
46
|
+
| -f | --fast | Maximum Fan Speed |
|
|
47
|
+
| -p | --poll-rate | Poll Rate for CPU Temperature in Seconds |
|
|
48
|
+
|
|
49
|
+
</details>
|
|
50
|
+
|
|
51
|
+
<details>
|
|
52
|
+
<summary><b>Example:</b></summary>
|
|
53
|
+
|
|
54
|
+
Python
|
|
55
|
+
```bash
|
|
56
|
+
fan-manager --intensity 5 --cold 50 --warm 80 --slow 5 --fast 100 --poll-rate 24
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Docker Compose
|
|
60
|
+
|
|
61
|
+
Fan Manager
|
|
62
|
+
```docker-compose
|
|
63
|
+
---
|
|
64
|
+
services:
|
|
65
|
+
fan-manager:
|
|
66
|
+
image: knucklessg1/fan-manager:latest
|
|
67
|
+
container_name: server_fan_speed
|
|
68
|
+
privileged: true
|
|
69
|
+
environment:
|
|
70
|
+
MODE: "fan-manager"
|
|
71
|
+
INTENSITY: ${INTENSITY}
|
|
72
|
+
COLD: ${COLD}
|
|
73
|
+
WARM: ${WARM}
|
|
74
|
+
SLOW: ${SLOW}
|
|
75
|
+
FAST: ${FAST}
|
|
76
|
+
POLL_RATE: ${POLL_RATE}
|
|
77
|
+
volumes:
|
|
78
|
+
- /dev/ipmi0:/dev/ipmi0
|
|
79
|
+
restart: unless-stopped
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Fan Manager MCP Server
|
|
83
|
+
```docker-compose
|
|
84
|
+
---
|
|
85
|
+
services:
|
|
86
|
+
fan-manager-mcp:
|
|
87
|
+
image: knucklessg1/fan-manager:latest
|
|
88
|
+
container_name: server_fan_speed
|
|
89
|
+
privileged: true
|
|
90
|
+
environment:
|
|
91
|
+
MODE: "fan-manager-mcp"
|
|
92
|
+
HOST: 0.0.0.0
|
|
93
|
+
PORT: 8030
|
|
94
|
+
TRANSPORT: "http"
|
|
95
|
+
volumes:
|
|
96
|
+
- /dev/ipmi0:/dev/ipmi0
|
|
97
|
+
restart: unless-stopped
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Docker Run
|
|
101
|
+
```bash
|
|
102
|
+
docker run -it -d knucklessg1/fan-manager:latest fan-manager
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Docker Compose
|
|
106
|
+
```bash
|
|
107
|
+
docker-compose up --build -d
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Use with AI
|
|
111
|
+
|
|
112
|
+
Configure `mcp.json`
|
|
113
|
+
|
|
114
|
+
Recommended: Store secrets in environment variables with lookup in JSON file.
|
|
115
|
+
|
|
116
|
+
For Testing Only: Plain text storage will also work, although **not** recommended.
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"mcpServers": {
|
|
121
|
+
"fan-manager": {
|
|
122
|
+
"command": "uv",
|
|
123
|
+
"args": [
|
|
124
|
+
"run",
|
|
125
|
+
"--with",
|
|
126
|
+
"fan-manager",
|
|
127
|
+
"fan-manager-mcp"
|
|
128
|
+
],
|
|
129
|
+
"timeout": 200000
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
</details>
|
|
136
|
+
|
|
137
|
+
<details>
|
|
138
|
+
<summary><b>Installation Instructions:</b></summary>
|
|
139
|
+
|
|
140
|
+
Install Python Package
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
python -m pip install fan-manager
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
</details>
|
|
147
|
+
|
|
148
|
+
<details>
|
|
149
|
+
<summary><b>Repository Owners:</b></summary>
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
<img width="100%" height="180em" src="https://github-readme-stats.vercel.app/api?username=Knucklessg1&show_icons=true&hide_border=true&&count_private=true&include_all_commits=true" />
|
|
153
|
+
|
|
154
|
+

|
|
155
|
+

|
|
156
|
+
</details>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
|
|
4
|
+
from fan_manager.fan_manager_mcp import fan_manager_mcp
|
|
5
|
+
from fan_manager.fan_manager import (
|
|
6
|
+
fan_manager,
|
|
7
|
+
setup_logging,
|
|
8
|
+
get_core_temp,
|
|
9
|
+
get_temp,
|
|
10
|
+
set_fan,
|
|
11
|
+
auto_set_fan_speed,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
fan-manager
|
|
16
|
+
|
|
17
|
+
Manager your Dell PowerEdge Fan Speed with this handy tool!
|
|
18
|
+
Support MCP Server
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"fan_manager",
|
|
23
|
+
"fan_manager_mcp",
|
|
24
|
+
"setup_logging",
|
|
25
|
+
"get_core_temp",
|
|
26
|
+
"get_temp",
|
|
27
|
+
"set_fan",
|
|
28
|
+
"auto_set_fan_speed",
|
|
29
|
+
]
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Union, Dict, Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def setup_logging(
|
|
14
|
+
is_mcp_server: bool = False, log_file: str = "fan_manager.log"
|
|
15
|
+
) -> None:
|
|
16
|
+
"""
|
|
17
|
+
Configure logging for the fan manager application.
|
|
18
|
+
"""
|
|
19
|
+
logging.basicConfig(
|
|
20
|
+
level=logging.DEBUG,
|
|
21
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
22
|
+
handlers=[
|
|
23
|
+
logging.FileHandler(
|
|
24
|
+
log_file if not is_mcp_server else "fan_manager_mcp.log"
|
|
25
|
+
),
|
|
26
|
+
logging.StreamHandler(sys.stdout),
|
|
27
|
+
],
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_core_temp(cpus: list, sensors: dict) -> Dict[str, Any]:
|
|
32
|
+
"""
|
|
33
|
+
Get the highest core temperature from the specified CPUs.
|
|
34
|
+
Returns a dictionary with response, command, and status.
|
|
35
|
+
"""
|
|
36
|
+
logger = logging.getLogger("FanManager")
|
|
37
|
+
highest_temp = 0.0
|
|
38
|
+
highest_core = 0
|
|
39
|
+
highest_cpu = ""
|
|
40
|
+
cores = 0
|
|
41
|
+
temp_cpu = 0.0
|
|
42
|
+
command = "sensors -j"
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
for cpu in cpus:
|
|
46
|
+
if cpu in sensors:
|
|
47
|
+
for key in sensors[cpu].keys():
|
|
48
|
+
if "Core" in key:
|
|
49
|
+
cores += 1
|
|
50
|
+
for temp_key in sensors[cpu][key].keys():
|
|
51
|
+
if "_input" in temp_key:
|
|
52
|
+
temp_cpu = sensors[cpu][key][temp_key]
|
|
53
|
+
if temp_cpu > highest_temp:
|
|
54
|
+
highest_temp = temp_cpu
|
|
55
|
+
highest_core = cores
|
|
56
|
+
highest_cpu = cpu
|
|
57
|
+
temp_cpu = highest_temp
|
|
58
|
+
logger.info(
|
|
59
|
+
f"Highest CPU: {highest_cpu}, Core: {highest_core}, Temperature: {highest_temp}"
|
|
60
|
+
)
|
|
61
|
+
return {"response": temp_cpu, "command": command, "status": 200}
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.error(f"Failed to get core temperature: {str(e)}")
|
|
64
|
+
return {"response": None, "command": command, "status": 500, "error": str(e)}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_temp() -> Dict[str, Any]:
|
|
68
|
+
"""
|
|
69
|
+
Get the current CPU temperature.
|
|
70
|
+
Returns a dictionary with response, command, and status.
|
|
71
|
+
"""
|
|
72
|
+
logger = logging.getLogger("FanManager")
|
|
73
|
+
command = "sensors -j"
|
|
74
|
+
try:
|
|
75
|
+
sensors = json.loads(os.popen("sensors -j").read())
|
|
76
|
+
cpus = ["coretemp-isa-0000", "coretemp-isa-0001"]
|
|
77
|
+
temp_result = get_core_temp(cpus, sensors)
|
|
78
|
+
if temp_result["status"] != 200:
|
|
79
|
+
raise RuntimeError(
|
|
80
|
+
temp_result.get("error", "Failed to get core temperature")
|
|
81
|
+
)
|
|
82
|
+
temp_cpu = temp_result["response"]
|
|
83
|
+
logger.info(f"Current Temperature: {temp_cpu}")
|
|
84
|
+
return {"response": temp_cpu, "command": command, "status": 200}
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.error(f"Failed to get temperature: {str(e)}")
|
|
87
|
+
return {"response": None, "command": command, "status": 500, "error": str(e)}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def set_fan(fan_level: int) -> Dict[str, Any]:
|
|
91
|
+
"""
|
|
92
|
+
Set the fan speed to the specified level.
|
|
93
|
+
Returns a dictionary with response, command, and status.
|
|
94
|
+
"""
|
|
95
|
+
logger = logging.getLogger("FanManager")
|
|
96
|
+
try:
|
|
97
|
+
if not (0 <= fan_level <= 100):
|
|
98
|
+
raise ValueError(f"Fan level {fan_level} is out of range (0-100)")
|
|
99
|
+
# Manual fan control
|
|
100
|
+
cmd1 = "ipmitool raw 0x30 0x30 0x01 0x00"
|
|
101
|
+
os.system(cmd1)
|
|
102
|
+
# Set fan level
|
|
103
|
+
cmd2 = f"ipmitool raw 0x30 0x30 0x02 0xff {hex(fan_level)}"
|
|
104
|
+
os.system(cmd2)
|
|
105
|
+
logger.info(f"Set fan level to {fan_level}")
|
|
106
|
+
return {"response": None, "command": f"{cmd1}; {cmd2}", "status": 200}
|
|
107
|
+
except ValueError as e:
|
|
108
|
+
logger.error(f"Invalid fan level: {str(e)}")
|
|
109
|
+
return {
|
|
110
|
+
"response": None,
|
|
111
|
+
"command": cmd2 if "cmd2" in locals() else "ipmitool raw",
|
|
112
|
+
"status": 400,
|
|
113
|
+
"error": str(e),
|
|
114
|
+
}
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logger.error(f"Failed to set fan level: {str(e)}")
|
|
117
|
+
return {
|
|
118
|
+
"response": None,
|
|
119
|
+
"command": cmd2 if "cmd2" in locals() else "ipmitool raw",
|
|
120
|
+
"status": 500,
|
|
121
|
+
"error": str(e),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def auto_set_fan_speed(
|
|
126
|
+
minimum_fan_speed: Union[int, float] = 5,
|
|
127
|
+
maximum_fan_speed: Union[int, float] = 100,
|
|
128
|
+
minimum_temperature: Union[int, float] = 50,
|
|
129
|
+
maximum_temperature: Union[int, float] = 80,
|
|
130
|
+
temperature_power: int = 5,
|
|
131
|
+
):
|
|
132
|
+
logger = logging.getLogger("FanManager")
|
|
133
|
+
logger.info("Starting fan manager service")
|
|
134
|
+
temp_result = get_temp()
|
|
135
|
+
if temp_result["status"] != 200:
|
|
136
|
+
logger.error(
|
|
137
|
+
f"Skipping fan adjustment due to temperature error: {temp_result.get('error', 'Unknown error')}"
|
|
138
|
+
)
|
|
139
|
+
cpu_temperature = temp_result["response"]
|
|
140
|
+
x: float = min(
|
|
141
|
+
1.0,
|
|
142
|
+
max(
|
|
143
|
+
0.0,
|
|
144
|
+
(cpu_temperature - minimum_temperature)
|
|
145
|
+
/ (maximum_temperature - minimum_temperature),
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
fan_level = int(
|
|
149
|
+
min(
|
|
150
|
+
maximum_fan_speed,
|
|
151
|
+
max(
|
|
152
|
+
minimum_fan_speed,
|
|
153
|
+
pow(x, temperature_power) * (maximum_fan_speed - minimum_fan_speed)
|
|
154
|
+
+ minimum_fan_speed,
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
fan_result = set_fan(fan_level)
|
|
159
|
+
if fan_result["status"] != 200:
|
|
160
|
+
logger.error(f"Failed to set fan: {fan_result.get('error', 'Unknown error')}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def run_service(
|
|
164
|
+
temperature_poll_rate: int = 24,
|
|
165
|
+
minimum_fan_speed: Union[int, float] = 5,
|
|
166
|
+
maximum_fan_speed: Union[int, float] = 100,
|
|
167
|
+
minimum_temperature: Union[int, float] = 50,
|
|
168
|
+
maximum_temperature: Union[int, float] = 80,
|
|
169
|
+
temperature_power: int = 5,
|
|
170
|
+
):
|
|
171
|
+
logger = logging.getLogger("FanManager")
|
|
172
|
+
logger.info("Starting fan manager service")
|
|
173
|
+
while True:
|
|
174
|
+
auto_set_fan_speed(
|
|
175
|
+
minimum_fan_speed=minimum_fan_speed,
|
|
176
|
+
maximum_fan_speed=maximum_fan_speed,
|
|
177
|
+
minimum_temperature=minimum_temperature,
|
|
178
|
+
maximum_temperature=maximum_temperature,
|
|
179
|
+
temperature_power=temperature_power,
|
|
180
|
+
)
|
|
181
|
+
time.sleep(temperature_poll_rate)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def usage():
|
|
185
|
+
logger = logging.getLogger("FanManager")
|
|
186
|
+
logger.info(
|
|
187
|
+
"Usage: \n"
|
|
188
|
+
"-h | --help [ See usage for fan-speed ]\n"
|
|
189
|
+
"-i | --intensity [ Intensity of Fan Speed - Scales Logarithmically (0-10) ]\n"
|
|
190
|
+
"-c | --cold [ Minimum Temperature for Fan Speed (40-90) ]\n"
|
|
191
|
+
"-w | --warm [ Maximum Temperature for Fan Speed (40-90) ]\n"
|
|
192
|
+
"-s | --slow [ Minimum Fan Speed (0-100) ]\n"
|
|
193
|
+
"-f | --fast [ Maximum Fan Speed (0-100) ]\n"
|
|
194
|
+
"-p | --poll-rate [ Poll Rate for CPU Temperature in Seconds (1-300) ]\n"
|
|
195
|
+
"\nExample: \n\t"
|
|
196
|
+
"fan-manager --intensity 5 --cold 50 --warm 80 --slow 5 --fast 100 --poll-rate 24\n"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def fan_manager():
|
|
201
|
+
setup_logging()
|
|
202
|
+
logger = logging.getLogger("FanManager")
|
|
203
|
+
logger.debug("Initializing fan manager")
|
|
204
|
+
|
|
205
|
+
# Define default values
|
|
206
|
+
defaults = {
|
|
207
|
+
"temperature_poll_rate": 24,
|
|
208
|
+
"minimum_fan_speed": 5,
|
|
209
|
+
"maximum_fan_speed": 100,
|
|
210
|
+
"minimum_temperature": 50,
|
|
211
|
+
"maximum_temperature": 80,
|
|
212
|
+
"temperature_power": 5,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# Set up argument parser
|
|
216
|
+
parser = argparse.ArgumentParser(
|
|
217
|
+
description="Fan manager tool to control fan speeds based on temperature.",
|
|
218
|
+
add_help=False,
|
|
219
|
+
)
|
|
220
|
+
parser.add_argument(
|
|
221
|
+
"-h",
|
|
222
|
+
"--help",
|
|
223
|
+
action="help",
|
|
224
|
+
default=argparse.SUPPRESS,
|
|
225
|
+
help="Show this help message and exit",
|
|
226
|
+
)
|
|
227
|
+
parser.add_argument(
|
|
228
|
+
"-i",
|
|
229
|
+
"--intensity",
|
|
230
|
+
type=int,
|
|
231
|
+
default=defaults["temperature_power"],
|
|
232
|
+
help="Temperature power intensity (default: %(default)s)",
|
|
233
|
+
)
|
|
234
|
+
parser.add_argument(
|
|
235
|
+
"-c",
|
|
236
|
+
"--cold",
|
|
237
|
+
type=int,
|
|
238
|
+
default=defaults["minimum_temperature"],
|
|
239
|
+
help="Minimum temperature (default: %(default)s)",
|
|
240
|
+
)
|
|
241
|
+
parser.add_argument(
|
|
242
|
+
"-w",
|
|
243
|
+
"--warm",
|
|
244
|
+
type=int,
|
|
245
|
+
default=defaults["maximum_temperature"],
|
|
246
|
+
help="Maximum temperature (default: %(default)s)",
|
|
247
|
+
)
|
|
248
|
+
parser.add_argument(
|
|
249
|
+
"-s",
|
|
250
|
+
"--slow",
|
|
251
|
+
type=int,
|
|
252
|
+
default=defaults["minimum_fan_speed"],
|
|
253
|
+
help="Minimum fan speed (default: %(default)s)",
|
|
254
|
+
)
|
|
255
|
+
parser.add_argument(
|
|
256
|
+
"-f",
|
|
257
|
+
"--fast",
|
|
258
|
+
type=int,
|
|
259
|
+
default=defaults["maximum_fan_speed"],
|
|
260
|
+
help="Maximum fan speed (default: %(default)s)",
|
|
261
|
+
)
|
|
262
|
+
parser.add_argument(
|
|
263
|
+
"-p",
|
|
264
|
+
"--poll-rate",
|
|
265
|
+
type=int,
|
|
266
|
+
default=defaults["temperature_poll_rate"],
|
|
267
|
+
help="Temperature poll rate (default: %(default)s)",
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
args = parser.parse_args()
|
|
272
|
+
except SystemExit:
|
|
273
|
+
usage()
|
|
274
|
+
sys.exit(2)
|
|
275
|
+
|
|
276
|
+
run_service(
|
|
277
|
+
temperature_poll_rate=args.poll_rate,
|
|
278
|
+
minimum_fan_speed=args.slow,
|
|
279
|
+
maximum_fan_speed=args.fast,
|
|
280
|
+
minimum_temperature=args.cold,
|
|
281
|
+
maximum_temperature=args.warm,
|
|
282
|
+
temperature_power=args.intensity,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
if __name__ == "__main__":
|
|
287
|
+
fan_manager()
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Dict, Any
|
|
8
|
+
from fastmcp import FastMCP, Context
|
|
9
|
+
from pydantic import Field
|
|
10
|
+
from fan_manager.fan_manager import (
|
|
11
|
+
get_temp,
|
|
12
|
+
set_fan,
|
|
13
|
+
setup_logging,
|
|
14
|
+
auto_set_fan_speed,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Initialize logging for MCP server
|
|
18
|
+
setup_logging(is_mcp_server=True, log_file="fan_manager_mcp.log")
|
|
19
|
+
|
|
20
|
+
mcp = FastMCP(name="FanManagerServer")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@mcp.tool(
|
|
24
|
+
annotations={
|
|
25
|
+
"title": "Get CPU Temperature",
|
|
26
|
+
"readOnlyHint": True,
|
|
27
|
+
"destructiveHint": False,
|
|
28
|
+
"idempotentHint": True,
|
|
29
|
+
"openWorldHint": False,
|
|
30
|
+
},
|
|
31
|
+
tags={"fan_management", "temperature"},
|
|
32
|
+
)
|
|
33
|
+
async def get_temperature(
|
|
34
|
+
ctx: Context = Field(
|
|
35
|
+
description="MCP context for progress reporting.", default=None
|
|
36
|
+
),
|
|
37
|
+
) -> Dict[str, Any]:
|
|
38
|
+
"""
|
|
39
|
+
Get the current CPU temperature.
|
|
40
|
+
Returns a dictionary with the temperature, command, and status.
|
|
41
|
+
"""
|
|
42
|
+
logger = logging.getLogger("FanManagerMCP")
|
|
43
|
+
logger.debug("Fetching CPU temperature")
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
if ctx:
|
|
47
|
+
await ctx.report_progress(progress=50, total=100)
|
|
48
|
+
logger.debug("Reported progress: 50/100")
|
|
49
|
+
result = get_temp()
|
|
50
|
+
if ctx:
|
|
51
|
+
await ctx.report_progress(progress=100, total=100)
|
|
52
|
+
logger.debug("Reported progress: 100/100")
|
|
53
|
+
logger.info(f"Temperature result: {result}")
|
|
54
|
+
return result
|
|
55
|
+
except Exception as e:
|
|
56
|
+
logger.error(f"Failed to get temperature: {str(e)}")
|
|
57
|
+
return {
|
|
58
|
+
"response": None,
|
|
59
|
+
"command": "sensors -j",
|
|
60
|
+
"status": 500,
|
|
61
|
+
"error": str(e),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@mcp.tool(
|
|
66
|
+
annotations={
|
|
67
|
+
"title": "Set Fan Speed",
|
|
68
|
+
"readOnlyHint": False,
|
|
69
|
+
"destructiveHint": True,
|
|
70
|
+
"idempotentHint": True,
|
|
71
|
+
"openWorldHint": False,
|
|
72
|
+
},
|
|
73
|
+
tags={"fan_management", "control"},
|
|
74
|
+
)
|
|
75
|
+
async def set_fan_speed(
|
|
76
|
+
fan_level: int = Field(description="Fan speed level (0-100)", ge=0, le=100),
|
|
77
|
+
ctx: Context = Field(
|
|
78
|
+
description="MCP context for progress reporting.", default=None
|
|
79
|
+
),
|
|
80
|
+
) -> Dict[str, Any]:
|
|
81
|
+
"""
|
|
82
|
+
Set the fan speed to the specified level.
|
|
83
|
+
Returns a dictionary with the response, command, and status.
|
|
84
|
+
"""
|
|
85
|
+
logger = logging.getLogger("FanManagerMCP")
|
|
86
|
+
logger.debug(f"Setting fan level to {fan_level}")
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
if ctx:
|
|
90
|
+
await ctx.report_progress(progress=50, total=100)
|
|
91
|
+
logger.debug("Reported progress: 50/100")
|
|
92
|
+
result = set_fan(fan_level)
|
|
93
|
+
if ctx:
|
|
94
|
+
await ctx.report_progress(progress=100, total=100)
|
|
95
|
+
logger.debug("Reported progress: 100/100")
|
|
96
|
+
logger.info(f"Set fan result: {result}")
|
|
97
|
+
return result
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.error(f"Failed to set fan level: {str(e)}")
|
|
100
|
+
return {
|
|
101
|
+
"response": None,
|
|
102
|
+
"command": f"ipmitool raw 0x30 0x30 0x02 0xff {hex(fan_level)}",
|
|
103
|
+
"status": 500,
|
|
104
|
+
"error": str(e),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@mcp.tool(
|
|
109
|
+
annotations={
|
|
110
|
+
"title": "Automatic Fan Speed Adjustment",
|
|
111
|
+
"readOnlyHint": False,
|
|
112
|
+
"destructiveHint": True,
|
|
113
|
+
"idempotentHint": True,
|
|
114
|
+
"openWorldHint": False,
|
|
115
|
+
},
|
|
116
|
+
tags={"fan_management", "control", "automatic"},
|
|
117
|
+
)
|
|
118
|
+
async def automatic_fan_speed(
|
|
119
|
+
minimum_fan_speed: float = Field(
|
|
120
|
+
description="Minimum fan speed (0-100)", default=5, ge=0, le=100
|
|
121
|
+
),
|
|
122
|
+
maximum_fan_speed: float = Field(
|
|
123
|
+
description="Maximum fan speed (0-100)", default=100, ge=0, le=100
|
|
124
|
+
),
|
|
125
|
+
minimum_temperature: float = Field(
|
|
126
|
+
description="Minimum temperature for fan speed adjustment (40-90)",
|
|
127
|
+
default=50,
|
|
128
|
+
ge=40,
|
|
129
|
+
le=90,
|
|
130
|
+
),
|
|
131
|
+
maximum_temperature: float = Field(
|
|
132
|
+
description="Maximum temperature for fan speed adjustment (40-90)",
|
|
133
|
+
default=80,
|
|
134
|
+
ge=40,
|
|
135
|
+
le=90,
|
|
136
|
+
),
|
|
137
|
+
temperature_power: int = Field(
|
|
138
|
+
description="Temperature power intensity for scaling (0-10)",
|
|
139
|
+
default=5,
|
|
140
|
+
ge=0,
|
|
141
|
+
le=10,
|
|
142
|
+
),
|
|
143
|
+
ctx: Context = Field(
|
|
144
|
+
description="MCP context for progress reporting.", default=None
|
|
145
|
+
),
|
|
146
|
+
) -> Dict[str, Any]:
|
|
147
|
+
"""
|
|
148
|
+
Automatically adjust fan speed based on current CPU temperature.
|
|
149
|
+
Returns a dictionary with the response, command, and status.
|
|
150
|
+
"""
|
|
151
|
+
logger = logging.getLogger("FanManagerMCP")
|
|
152
|
+
logger.debug(
|
|
153
|
+
f"Starting automatic fan speed adjustment with params: "
|
|
154
|
+
f"min_fan={minimum_fan_speed}, max_fan={maximum_fan_speed}, "
|
|
155
|
+
f"min_temp={minimum_temperature}, max_temp={maximum_temperature}, "
|
|
156
|
+
f"power={temperature_power}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
if ctx:
|
|
161
|
+
await ctx.report_progress(progress=50, total=100)
|
|
162
|
+
logger.debug("Reported progress: 50/100")
|
|
163
|
+
result = auto_set_fan_speed(
|
|
164
|
+
minimum_fan_speed=minimum_fan_speed,
|
|
165
|
+
maximum_fan_speed=maximum_fan_speed,
|
|
166
|
+
minimum_temperature=minimum_temperature,
|
|
167
|
+
maximum_temperature=maximum_temperature,
|
|
168
|
+
temperature_power=temperature_power,
|
|
169
|
+
)
|
|
170
|
+
if ctx:
|
|
171
|
+
await ctx.report_progress(progress=100, total=100)
|
|
172
|
+
logger.debug("Reported progress: 100/100")
|
|
173
|
+
logger.info(f"Automatic fan speed result: {result}")
|
|
174
|
+
return {"response": result, "command": "auto_set_fan_speed", "status": 200}
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.error(f"Failed to adjust fan speed automatically: {str(e)}")
|
|
177
|
+
return {
|
|
178
|
+
"response": None,
|
|
179
|
+
"command": "auto_set_fan_speed",
|
|
180
|
+
"status": 500,
|
|
181
|
+
"error": str(e),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def fan_manager_mcp():
|
|
186
|
+
logger = logging.getLogger("FanManagerMCP")
|
|
187
|
+
logger.debug("Starting fan manager MCP server")
|
|
188
|
+
|
|
189
|
+
parser = argparse.ArgumentParser(description="Run fan manager MCP server.")
|
|
190
|
+
parser.add_argument(
|
|
191
|
+
"-t",
|
|
192
|
+
"--transport",
|
|
193
|
+
default="stdio",
|
|
194
|
+
choices=["stdio", "http", "sse"],
|
|
195
|
+
help="Transport method: 'stdio', 'http', or 'sse' (default: stdio)",
|
|
196
|
+
)
|
|
197
|
+
parser.add_argument(
|
|
198
|
+
"-s",
|
|
199
|
+
"--host",
|
|
200
|
+
default="0.0.0.0",
|
|
201
|
+
help="Host address for HTTP transport (default: 0.0.0.0)",
|
|
202
|
+
)
|
|
203
|
+
parser.add_argument(
|
|
204
|
+
"-p",
|
|
205
|
+
"--port",
|
|
206
|
+
type=int,
|
|
207
|
+
default=8030,
|
|
208
|
+
help="Port number for HTTP transport (default: 8030)",
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
args = parser.parse_args()
|
|
212
|
+
|
|
213
|
+
if args.port < 0 or args.port > 65535:
|
|
214
|
+
logger.error(f"Port {args.port} is out of valid range (0-65535)")
|
|
215
|
+
sys.exit(1)
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
if args.transport == "stdio":
|
|
219
|
+
mcp.run(transport="stdio")
|
|
220
|
+
elif args.transport == "http":
|
|
221
|
+
mcp.run(transport="http", host=args.host, port=args.port)
|
|
222
|
+
elif args.transport == "sse":
|
|
223
|
+
mcp.run(transport="sse", host=args.host, port=args.port)
|
|
224
|
+
else:
|
|
225
|
+
logger.error("Transport not supported")
|
|
226
|
+
sys.exit(1)
|
|
227
|
+
except Exception as e:
|
|
228
|
+
logger.error(f"Failed to start MCP server: {str(e)}")
|
|
229
|
+
sys.exit(1)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
if __name__ == "__main__":
|
|
233
|
+
fan_manager_mcp()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def fan_manager_mcp():
|
|
237
|
+
logger = logging.getLogger("FanManagerMCP")
|
|
238
|
+
logger.debug("Starting fan manager MCP server")
|
|
239
|
+
|
|
240
|
+
parser = argparse.ArgumentParser(description="Run fan manager MCP server.")
|
|
241
|
+
parser.add_argument(
|
|
242
|
+
"-t",
|
|
243
|
+
"--transport",
|
|
244
|
+
default="stdio",
|
|
245
|
+
choices=["stdio", "http", "sse"],
|
|
246
|
+
help="Transport method: 'stdio', 'http', or 'sse' (default: stdio)",
|
|
247
|
+
)
|
|
248
|
+
parser.add_argument(
|
|
249
|
+
"-s",
|
|
250
|
+
"--host",
|
|
251
|
+
default="0.0.0.0",
|
|
252
|
+
help="Host address for HTTP transport (default: 0.0.0.0)",
|
|
253
|
+
)
|
|
254
|
+
parser.add_argument(
|
|
255
|
+
"-p",
|
|
256
|
+
"--port",
|
|
257
|
+
type=int,
|
|
258
|
+
default=8030,
|
|
259
|
+
help="Port number for HTTP transport (default: 8030)",
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
args = parser.parse_args()
|
|
263
|
+
|
|
264
|
+
if args.port < 0 or args.port > 65535:
|
|
265
|
+
logger.error(f"Port {args.port} is out of valid range (0-65535)")
|
|
266
|
+
sys.exit(1)
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
if args.transport == "stdio":
|
|
270
|
+
mcp.run(transport="stdio")
|
|
271
|
+
elif args.transport == "http":
|
|
272
|
+
mcp.run(transport="http", host=args.host, port=args.port)
|
|
273
|
+
elif args.transport == "sse":
|
|
274
|
+
mcp.run(transport="sse", host=args.host, port=args.port)
|
|
275
|
+
else:
|
|
276
|
+
logger.error("Transport not supported")
|
|
277
|
+
sys.exit(1)
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.error(f"Failed to start MCP server: {str(e)}")
|
|
280
|
+
sys.exit(1)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
if __name__ == "__main__":
|
|
284
|
+
fan_manager_mcp()
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fan-manager
|
|
3
|
+
Version: 0.6.3
|
|
4
|
+
Summary: Manager your Dell PowerEdge Fan Speed with this handy tool and MCP Server!
|
|
5
|
+
Author-email: Audel Rouhi <knucklessg1@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
8
|
+
Classifier: License :: Public Domain
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# Fan-Manager
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+

|
|
21
|
+

|
|
22
|
+

|
|
23
|
+

|
|
24
|
+

|
|
25
|
+

|
|
26
|
+
|
|
27
|
+

|
|
28
|
+

|
|
29
|
+

|
|
30
|
+

|
|
31
|
+
|
|
32
|
+

|
|
33
|
+

|
|
34
|
+

|
|
35
|
+

|
|
36
|
+

|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
*Version: 0.6.3*
|
|
40
|
+
|
|
41
|
+
Manager your Dell PowerEdge Fan Speed with this handy tool!
|
|
42
|
+
|
|
43
|
+
MCP Server for Agentic AI! Get started with Pip or Docker as well
|
|
44
|
+
|
|
45
|
+
This repository is actively maintained - Contributions are welcome!
|
|
46
|
+
|
|
47
|
+
Contribution Opportunities:
|
|
48
|
+
- Increase support of Dell PowerEdge Devices
|
|
49
|
+
- Support Non-PowerEdge Devices
|
|
50
|
+
- Support Non-Dell Devices
|
|
51
|
+
|
|
52
|
+
<details>
|
|
53
|
+
<summary><b>Usage:</b></summary>
|
|
54
|
+
|
|
55
|
+
| Short Flag | Long Flag | Description |
|
|
56
|
+
|------------|-------------|--------------------------------------------------------|
|
|
57
|
+
| -h | --help | See usage for fan-manager |
|
|
58
|
+
| -i | --intensity | Intensity of Fan Speed - Scales Logarithmically (0-10) |
|
|
59
|
+
| -c | --cold | Minimum Temperature for Fan Speed |
|
|
60
|
+
| -w | --warm | Maximum Temperature for Fan Speed |
|
|
61
|
+
| -s | --slow | Minimum Fan Speed |
|
|
62
|
+
| -f | --fast | Maximum Fan Speed |
|
|
63
|
+
| -p | --poll-rate | Poll Rate for CPU Temperature in Seconds |
|
|
64
|
+
|
|
65
|
+
</details>
|
|
66
|
+
|
|
67
|
+
<details>
|
|
68
|
+
<summary><b>Example:</b></summary>
|
|
69
|
+
|
|
70
|
+
Python
|
|
71
|
+
```bash
|
|
72
|
+
fan-manager --intensity 5 --cold 50 --warm 80 --slow 5 --fast 100 --poll-rate 24
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Docker Compose
|
|
76
|
+
|
|
77
|
+
Fan Manager
|
|
78
|
+
```docker-compose
|
|
79
|
+
---
|
|
80
|
+
services:
|
|
81
|
+
fan-manager:
|
|
82
|
+
image: knucklessg1/fan-manager:latest
|
|
83
|
+
container_name: server_fan_speed
|
|
84
|
+
privileged: true
|
|
85
|
+
environment:
|
|
86
|
+
MODE: "fan-manager"
|
|
87
|
+
INTENSITY: ${INTENSITY}
|
|
88
|
+
COLD: ${COLD}
|
|
89
|
+
WARM: ${WARM}
|
|
90
|
+
SLOW: ${SLOW}
|
|
91
|
+
FAST: ${FAST}
|
|
92
|
+
POLL_RATE: ${POLL_RATE}
|
|
93
|
+
volumes:
|
|
94
|
+
- /dev/ipmi0:/dev/ipmi0
|
|
95
|
+
restart: unless-stopped
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Fan Manager MCP Server
|
|
99
|
+
```docker-compose
|
|
100
|
+
---
|
|
101
|
+
services:
|
|
102
|
+
fan-manager-mcp:
|
|
103
|
+
image: knucklessg1/fan-manager:latest
|
|
104
|
+
container_name: server_fan_speed
|
|
105
|
+
privileged: true
|
|
106
|
+
environment:
|
|
107
|
+
MODE: "fan-manager-mcp"
|
|
108
|
+
HOST: 0.0.0.0
|
|
109
|
+
PORT: 8030
|
|
110
|
+
TRANSPORT: "http"
|
|
111
|
+
volumes:
|
|
112
|
+
- /dev/ipmi0:/dev/ipmi0
|
|
113
|
+
restart: unless-stopped
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Docker Run
|
|
117
|
+
```bash
|
|
118
|
+
docker run -it -d knucklessg1/fan-manager:latest fan-manager
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Docker Compose
|
|
122
|
+
```bash
|
|
123
|
+
docker-compose up --build -d
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Use with AI
|
|
127
|
+
|
|
128
|
+
Configure `mcp.json`
|
|
129
|
+
|
|
130
|
+
Recommended: Store secrets in environment variables with lookup in JSON file.
|
|
131
|
+
|
|
132
|
+
For Testing Only: Plain text storage will also work, although **not** recommended.
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"mcpServers": {
|
|
137
|
+
"fan-manager": {
|
|
138
|
+
"command": "uv",
|
|
139
|
+
"args": [
|
|
140
|
+
"run",
|
|
141
|
+
"--with",
|
|
142
|
+
"fan-manager",
|
|
143
|
+
"fan-manager-mcp"
|
|
144
|
+
],
|
|
145
|
+
"timeout": 200000
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
</details>
|
|
152
|
+
|
|
153
|
+
<details>
|
|
154
|
+
<summary><b>Installation Instructions:</b></summary>
|
|
155
|
+
|
|
156
|
+
Install Python Package
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
python -m pip install fan-manager
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
</details>
|
|
163
|
+
|
|
164
|
+
<details>
|
|
165
|
+
<summary><b>Repository Owners:</b></summary>
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
<img width="100%" height="180em" src="https://github-readme-stats.vercel.app/api?username=Knucklessg1&show_icons=true&hide_border=true&&count_private=true&include_all_commits=true" />
|
|
169
|
+
|
|
170
|
+

|
|
171
|
+

|
|
172
|
+
</details>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
requirements.txt
|
|
6
|
+
fan_manager/__init__.py
|
|
7
|
+
fan_manager/__main__.py
|
|
8
|
+
fan_manager/fan_manager.py
|
|
9
|
+
fan_manager/fan_manager_mcp.py
|
|
10
|
+
fan_manager.egg-info/PKG-INFO
|
|
11
|
+
fan_manager.egg-info/SOURCES.txt
|
|
12
|
+
fan_manager.egg-info/dependency_links.txt
|
|
13
|
+
fan_manager.egg-info/entry_points.txt
|
|
14
|
+
fan_manager.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=80.9.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fan-manager"
|
|
7
|
+
version = "0.6.3"
|
|
8
|
+
description = "Manager your Dell PowerEdge Fan Speed with this handy tool and MCP Server!"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{ name = "Audel Rouhi", email = "knucklessg1@gmail.com" }]
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 5 - Production/Stable",
|
|
14
|
+
"License :: Public Domain",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Operating System :: POSIX :: Linux",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
]
|
|
19
|
+
requires-python = ">=3.8"
|
|
20
|
+
dependencies = []
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
fan-manager = "fan_manager.fan_manager:fan_manager"
|
|
24
|
+
fan-manager-mcp = "fan_manager.fan_manager_mcp:fan_manager_mcp"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["."]
|
|
28
|
+
|
|
29
|
+
[tool.setuptools]
|
|
30
|
+
include-package-data = true
|
|
31
|
+
package-data = { "fan_manager" = ["fan_manager"] }
|
|
File without changes
|