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.
@@ -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
+ ![PyPI - Version](https://img.shields.io/pypi/v/fan-manager)
20
+ ![PyPI - Downloads](https://img.shields.io/pypi/dd/fan-manager)
21
+ ![GitHub Repo stars](https://img.shields.io/github/stars/Knuckles-Team/fan-manager)
22
+ ![GitHub forks](https://img.shields.io/github/forks/Knuckles-Team/fan-manager)
23
+ ![GitHub contributors](https://img.shields.io/github/contributors/Knuckles-Team/fan-manager)
24
+ ![PyPI - License](https://img.shields.io/pypi/l/fan-manager)
25
+ ![GitHub](https://img.shields.io/github/license/Knuckles-Team/fan-manager)
26
+
27
+ ![GitHub last commit (by committer)](https://img.shields.io/github/last-commit/Knuckles-Team/fan-manager)
28
+ ![GitHub pull requests](https://img.shields.io/github/issues-pr/Knuckles-Team/fan-manager)
29
+ ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed/Knuckles-Team/fan-manager)
30
+ ![GitHub issues](https://img.shields.io/github/issues/Knuckles-Team/fan-manager)
31
+
32
+ ![GitHub top language](https://img.shields.io/github/languages/top/Knuckles-Team/fan-manager)
33
+ ![GitHub language count](https://img.shields.io/github/languages/count/Knuckles-Team/fan-manager)
34
+ ![GitHub repo size](https://img.shields.io/github/repo-size/Knuckles-Team/fan-manager)
35
+ ![GitHub repo file count (file type)](https://img.shields.io/github/directory-file-count/Knuckles-Team/fan-manager)
36
+ ![PyPI - Wheel](https://img.shields.io/pypi/wheel/fan-manager)
37
+ ![PyPI - Implementation](https://img.shields.io/pypi/implementation/fan-manager)
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
+ ![GitHub followers](https://img.shields.io/github/followers/Knucklessg1)
171
+ ![GitHub User's stars](https://img.shields.io/github/stars/Knucklessg1)
172
+ </details>
@@ -0,0 +1,156 @@
1
+ # Fan-Manager
2
+
3
+ ![PyPI - Version](https://img.shields.io/pypi/v/fan-manager)
4
+ ![PyPI - Downloads](https://img.shields.io/pypi/dd/fan-manager)
5
+ ![GitHub Repo stars](https://img.shields.io/github/stars/Knuckles-Team/fan-manager)
6
+ ![GitHub forks](https://img.shields.io/github/forks/Knuckles-Team/fan-manager)
7
+ ![GitHub contributors](https://img.shields.io/github/contributors/Knuckles-Team/fan-manager)
8
+ ![PyPI - License](https://img.shields.io/pypi/l/fan-manager)
9
+ ![GitHub](https://img.shields.io/github/license/Knuckles-Team/fan-manager)
10
+
11
+ ![GitHub last commit (by committer)](https://img.shields.io/github/last-commit/Knuckles-Team/fan-manager)
12
+ ![GitHub pull requests](https://img.shields.io/github/issues-pr/Knuckles-Team/fan-manager)
13
+ ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed/Knuckles-Team/fan-manager)
14
+ ![GitHub issues](https://img.shields.io/github/issues/Knuckles-Team/fan-manager)
15
+
16
+ ![GitHub top language](https://img.shields.io/github/languages/top/Knuckles-Team/fan-manager)
17
+ ![GitHub language count](https://img.shields.io/github/languages/count/Knuckles-Team/fan-manager)
18
+ ![GitHub repo size](https://img.shields.io/github/repo-size/Knuckles-Team/fan-manager)
19
+ ![GitHub repo file count (file type)](https://img.shields.io/github/directory-file-count/Knuckles-Team/fan-manager)
20
+ ![PyPI - Wheel](https://img.shields.io/pypi/wheel/fan-manager)
21
+ ![PyPI - Implementation](https://img.shields.io/pypi/implementation/fan-manager)
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
+ ![GitHub followers](https://img.shields.io/github/followers/Knucklessg1)
155
+ ![GitHub User's stars](https://img.shields.io/github/stars/Knucklessg1)
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,8 @@
1
+ #!/usr/bin/python
2
+ # coding: utf-8
3
+ from fan_manager.fan_manager import fan_manager
4
+ from fan_manager.fan_manager_mcp import fan_manager_mcp
5
+
6
+ if __name__ == "__main__":
7
+ fan_manager()
8
+ fan_manager_mcp()
@@ -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
+ ![PyPI - Version](https://img.shields.io/pypi/v/fan-manager)
20
+ ![PyPI - Downloads](https://img.shields.io/pypi/dd/fan-manager)
21
+ ![GitHub Repo stars](https://img.shields.io/github/stars/Knuckles-Team/fan-manager)
22
+ ![GitHub forks](https://img.shields.io/github/forks/Knuckles-Team/fan-manager)
23
+ ![GitHub contributors](https://img.shields.io/github/contributors/Knuckles-Team/fan-manager)
24
+ ![PyPI - License](https://img.shields.io/pypi/l/fan-manager)
25
+ ![GitHub](https://img.shields.io/github/license/Knuckles-Team/fan-manager)
26
+
27
+ ![GitHub last commit (by committer)](https://img.shields.io/github/last-commit/Knuckles-Team/fan-manager)
28
+ ![GitHub pull requests](https://img.shields.io/github/issues-pr/Knuckles-Team/fan-manager)
29
+ ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed/Knuckles-Team/fan-manager)
30
+ ![GitHub issues](https://img.shields.io/github/issues/Knuckles-Team/fan-manager)
31
+
32
+ ![GitHub top language](https://img.shields.io/github/languages/top/Knuckles-Team/fan-manager)
33
+ ![GitHub language count](https://img.shields.io/github/languages/count/Knuckles-Team/fan-manager)
34
+ ![GitHub repo size](https://img.shields.io/github/repo-size/Knuckles-Team/fan-manager)
35
+ ![GitHub repo file count (file type)](https://img.shields.io/github/directory-file-count/Knuckles-Team/fan-manager)
36
+ ![PyPI - Wheel](https://img.shields.io/pypi/wheel/fan-manager)
37
+ ![PyPI - Implementation](https://img.shields.io/pypi/implementation/fan-manager)
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
+ ![GitHub followers](https://img.shields.io/github/followers/Knucklessg1)
171
+ ![GitHub User's stars](https://img.shields.io/github/stars/Knucklessg1)
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,3 @@
1
+ [console_scripts]
2
+ fan-manager = fan_manager.fan_manager:fan_manager
3
+ fan-manager-mcp = fan_manager.fan_manager_mcp:fan_manager_mcp
@@ -0,0 +1,2 @@
1
+ dist
2
+ fan_manager
@@ -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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+