smfc 4.0.0b1__py3-none-any.whl
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.
Potentially problematic release.
This version of smfc might be problematic. Click here for more details.
- smfc/__init__.py +15 -0
- smfc/cmd.py +18 -0
- smfc/cpuzone.py +87 -0
- smfc/fancontroller.py +273 -0
- smfc/hdzone.py +307 -0
- smfc/ipmi.py +242 -0
- smfc/log.py +119 -0
- smfc/service.py +225 -0
- smfc-4.0.0b1.dist-info/METADATA +595 -0
- smfc-4.0.0b1.dist-info/RECORD +14 -0
- smfc-4.0.0b1.dist-info/WHEEL +5 -0
- smfc-4.0.0b1.dist-info/entry_points.txt +2 -0
- smfc-4.0.0b1.dist-info/licenses/LICENSE +674 -0
- smfc-4.0.0b1.dist-info/top_level.txt +1 -0
smfc/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#
|
|
2
|
+
# __init__.py (C) 2020-2025, Peter Sulyok
|
|
3
|
+
# smfc package: Super Micro fan control for Linux (home) servers.
|
|
4
|
+
#
|
|
5
|
+
from smfc.log import Log
|
|
6
|
+
from smfc.ipmi import Ipmi
|
|
7
|
+
from smfc.fancontroller import FanController
|
|
8
|
+
from smfc.cpuzone import CpuZone
|
|
9
|
+
from smfc.hdzone import HdZone
|
|
10
|
+
from smfc.service import Service
|
|
11
|
+
from smfc.cmd import main
|
|
12
|
+
|
|
13
|
+
__all__ = [ "Log", "Ipmi", "FanController", "CpuZone", "HdZone", "Service", "main"]
|
|
14
|
+
|
|
15
|
+
# End.
|
smfc/cmd.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#
|
|
2
|
+
# cmd.py (C) 2020-2025, Peter Sulyok
|
|
3
|
+
# smfc package: Super Micro fan control for Linux (home) servers.
|
|
4
|
+
# smfc.main() function implementation, command-line interface.
|
|
5
|
+
#
|
|
6
|
+
from smfc.service import Service
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main() -> None:
|
|
10
|
+
"""Entry point of the `smfc` program."""
|
|
11
|
+
service = Service()
|
|
12
|
+
service.run()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if __name__ == '__main__':
|
|
16
|
+
main()
|
|
17
|
+
|
|
18
|
+
# End.
|
smfc/cpuzone.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#
|
|
2
|
+
# cpuzone.py (C) 2020-2025, Peter Sulyok
|
|
3
|
+
# smfc package: Super Micro fan control for Linux (home) servers.
|
|
4
|
+
# smfc.CpuZone() class implementation.
|
|
5
|
+
#
|
|
6
|
+
from configparser import ConfigParser
|
|
7
|
+
from pyudev import Context
|
|
8
|
+
from smfc.fancontroller import FanController
|
|
9
|
+
from smfc.ipmi import Ipmi
|
|
10
|
+
from smfc.log import Log
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CpuZone(FanController):
|
|
14
|
+
"""CPU zone fan control."""
|
|
15
|
+
|
|
16
|
+
# Constant values for the configuration parameters.
|
|
17
|
+
CS_CPU_ZONE: str = 'CPU zone'
|
|
18
|
+
CV_CPU_ZONE_ENABLED: str = 'enabled'
|
|
19
|
+
CV_CPU_ZONE_TEMP_CALC: str = 'temp_calc'
|
|
20
|
+
CV_CPU_ZONE_STEPS: str = 'steps'
|
|
21
|
+
CV_CPU_ZONE_SENSITIVITY: str = 'sensitivity'
|
|
22
|
+
CV_CPU_ZONE_POLLING: str = 'polling'
|
|
23
|
+
CV_CPU_ZONE_MIN_TEMP: str = 'min_temp'
|
|
24
|
+
CV_CPU_ZONE_MAX_TEMP: str = 'max_temp'
|
|
25
|
+
CV_CPU_ZONE_MIN_LEVEL: str = 'min_level'
|
|
26
|
+
CV_CPU_ZONE_MAX_LEVEL: str = 'max_level'
|
|
27
|
+
|
|
28
|
+
def __init__(self, log: Log, udevc: Context, ipmi: Ipmi, config:ConfigParser) -> None:
|
|
29
|
+
"""Initialize the CpuZone class and raise exception in case of invalid configuration.
|
|
30
|
+
Args:
|
|
31
|
+
log (Log): reference to a Log class instance
|
|
32
|
+
udevc (Context): reference to an udev database connection (instance of Context from pyudev)
|
|
33
|
+
ipmi (Ipmi): reference to an Ipmi class instance
|
|
34
|
+
config (ConfigParser): reference to the configuration (default=None)
|
|
35
|
+
Raises:
|
|
36
|
+
ValueError: multiple hwmon devices reported, one expected
|
|
37
|
+
RuntimeError: No HWMON device found for CPU(s)
|
|
38
|
+
"""
|
|
39
|
+
count: int # CPU count.
|
|
40
|
+
|
|
41
|
+
# Build the list of paths for hwmon devices.
|
|
42
|
+
self.hwmon_path = []
|
|
43
|
+
# We are looking for either Intel (coretemp) or AMD (k10temp) CPUs.
|
|
44
|
+
for dev_filter in [{'MODALIAS':'platform:coretemp'}, {'DRIVER':'k10temp'}]:
|
|
45
|
+
self.hwmon_path = [self.get_hwmon_path(udevc, dev) for dev in udevc.list_devices(**dev_filter)]
|
|
46
|
+
# If we found results.
|
|
47
|
+
if self.hwmon_path:
|
|
48
|
+
break
|
|
49
|
+
if not self.hwmon_path:
|
|
50
|
+
raise RuntimeError('pyudev: No HWMON device(s) can be found for the CPU.')
|
|
51
|
+
# Calculate count.
|
|
52
|
+
count = len(self.hwmon_path)
|
|
53
|
+
|
|
54
|
+
# Initialize FanController class.
|
|
55
|
+
super().__init__(log, ipmi, Ipmi.CPU_ZONE, CpuZone.CS_CPU_ZONE, count,
|
|
56
|
+
config[CpuZone.CS_CPU_ZONE].getint(CpuZone.CV_CPU_ZONE_TEMP_CALC, fallback=FanController.CALC_AVG),
|
|
57
|
+
config[CpuZone.CS_CPU_ZONE].getint(CpuZone.CV_CPU_ZONE_STEPS, fallback=6),
|
|
58
|
+
config[CpuZone.CS_CPU_ZONE].getfloat(CpuZone.CV_CPU_ZONE_SENSITIVITY, fallback=3.0),
|
|
59
|
+
config[CpuZone.CS_CPU_ZONE].getfloat(CpuZone.CV_CPU_ZONE_POLLING, fallback=2),
|
|
60
|
+
config[CpuZone.CS_CPU_ZONE].getfloat(CpuZone.CV_CPU_ZONE_MIN_TEMP, fallback=30.0),
|
|
61
|
+
config[CpuZone.CS_CPU_ZONE].getfloat(CpuZone.CV_CPU_ZONE_MAX_TEMP, fallback=60.0),
|
|
62
|
+
config[CpuZone.CS_CPU_ZONE].getint(CpuZone.CV_CPU_ZONE_MIN_LEVEL, fallback=35),
|
|
63
|
+
config[CpuZone.CS_CPU_ZONE].getint(CpuZone.CV_CPU_ZONE_MAX_LEVEL, fallback=100)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def _get_nth_temp(self, index: int) -> float:
|
|
67
|
+
"""Get the temperature of the 'nth' element in the hwmon list.
|
|
68
|
+
Args:
|
|
69
|
+
index (int): index in hwmon list
|
|
70
|
+
Returns:
|
|
71
|
+
float: temperature value
|
|
72
|
+
Raises:
|
|
73
|
+
FileNotFoundError: file not found
|
|
74
|
+
IOError: file cannot be opened
|
|
75
|
+
ValueError: invalid value read from file
|
|
76
|
+
IndexError: invalid index
|
|
77
|
+
"""
|
|
78
|
+
value: float # Temperature value
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
with open(self.hwmon_path[index], "r", encoding="UTF-8") as f:
|
|
82
|
+
value = float(f.read()) / 1000
|
|
83
|
+
except (IOError, FileNotFoundError, ValueError) as e:
|
|
84
|
+
raise e
|
|
85
|
+
return value
|
|
86
|
+
|
|
87
|
+
# End.
|
smfc/fancontroller.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#
|
|
2
|
+
# fancontroller.py (C) 2020-2025, Peter Sulyok
|
|
3
|
+
# smfc package: Super Micro fan control for Linux (home) servers.
|
|
4
|
+
# smfc.FanController() class implementation.
|
|
5
|
+
#
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from typing import List, Callable
|
|
9
|
+
from pyudev import Context, Device
|
|
10
|
+
from smfc.ipmi import Ipmi
|
|
11
|
+
from smfc.log import Log
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FanController:
|
|
15
|
+
"""Generic fan controller class for an IPMI zone."""
|
|
16
|
+
|
|
17
|
+
# Constant values for temperature calculation
|
|
18
|
+
CALC_MIN: int = 0
|
|
19
|
+
CALC_AVG: int = 1
|
|
20
|
+
CALC_MAX: int = 2
|
|
21
|
+
|
|
22
|
+
# Configuration parameters
|
|
23
|
+
log: Log # Reference to a Log class instance
|
|
24
|
+
ipmi: Ipmi # Reference to an Ipmi class instance
|
|
25
|
+
ipmi_zone: int # IPMI zone identifier
|
|
26
|
+
name: str # Name of the controller
|
|
27
|
+
count: int # Number of controlled entities
|
|
28
|
+
temp_calc: int # Calculate of the temperature (0-min, 1-avg, 2-max)
|
|
29
|
+
steps: int # Discrete steps in temperatures and fan levels
|
|
30
|
+
sensitivity: float # Temperature change to activate fan controller (C)
|
|
31
|
+
polling: float # Polling interval to read temperature (sec)
|
|
32
|
+
min_temp: float # Minimum temperature value (C)
|
|
33
|
+
max_temp: float # Maximum temperature value (C)
|
|
34
|
+
min_level: int # Minimum fan level (0..100%)
|
|
35
|
+
max_level: int # Maximum fan level (0..100%)
|
|
36
|
+
hwmon_path: List[str] # List of paths for HWMON devices
|
|
37
|
+
|
|
38
|
+
# Measured or calculated attributes
|
|
39
|
+
temp_step: float # A temperature steps value (C)
|
|
40
|
+
level_step: float # A fan level step value (0..100%)
|
|
41
|
+
last_time: float # Last system time we polled temperature (timestamp)
|
|
42
|
+
last_temp: float # Last measured temperature value (C)
|
|
43
|
+
last_level: int # Last configured fan level (0..100%)
|
|
44
|
+
|
|
45
|
+
# Function variable for selected temperature calculation method
|
|
46
|
+
get_temp_func: Callable[[], float]
|
|
47
|
+
|
|
48
|
+
def __init__(self, log: Log, ipmi: Ipmi, ipmi_zone: int, name: str, count: int, temp_calc: int,
|
|
49
|
+
steps: int, sensitivity: float, polling: float, min_temp: float, max_temp: float, min_level: int,
|
|
50
|
+
max_level: int) -> None:
|
|
51
|
+
"""Initialize the FanController class. Will raise an exception in case of invalid parameters.
|
|
52
|
+
Args:
|
|
53
|
+
log (Log): reference to a Log class instance
|
|
54
|
+
ipmi (Ipmi): reference to an Ipmi class instance
|
|
55
|
+
ipmi_zone (int): IPMI zone identifier
|
|
56
|
+
name (str): name of the controller
|
|
57
|
+
count (int): number of devices
|
|
58
|
+
temp_calc (int): calculation of temperature
|
|
59
|
+
steps (int): discrete steps in temperatures and fan levels
|
|
60
|
+
sensitivity (float): temperature change to activate fan controller (C)
|
|
61
|
+
polling (float): polling time interval for reading temperature (sec)
|
|
62
|
+
min_temp (float): minimum temperature value (C)
|
|
63
|
+
max_temp (float): maximum temperature value (C)
|
|
64
|
+
min_level (int): minimum fan level value [0..100%]
|
|
65
|
+
max_level (int): maximum fan level value [0..100%]
|
|
66
|
+
Raises:
|
|
67
|
+
ValueError: invalid input parameter
|
|
68
|
+
"""
|
|
69
|
+
# Save and validate configuration parameters.
|
|
70
|
+
self.log = log
|
|
71
|
+
self.ipmi = ipmi
|
|
72
|
+
self.ipmi_zone = ipmi_zone
|
|
73
|
+
if self.ipmi_zone not in {Ipmi.CPU_ZONE, Ipmi.HD_ZONE}:
|
|
74
|
+
raise ValueError('invalid value: ipmi_zone')
|
|
75
|
+
self.name = name
|
|
76
|
+
self.count = count
|
|
77
|
+
if self.count <= 0:
|
|
78
|
+
raise ValueError('count <= 0')
|
|
79
|
+
self.temp_calc = temp_calc
|
|
80
|
+
if self.temp_calc not in {self.CALC_MIN, self.CALC_AVG, self.CALC_MAX}:
|
|
81
|
+
raise ValueError('invalid value: temp_calc')
|
|
82
|
+
self.steps = steps
|
|
83
|
+
if self.steps <= 0:
|
|
84
|
+
raise ValueError('steps <= 0')
|
|
85
|
+
self.sensitivity = sensitivity
|
|
86
|
+
if self.sensitivity <= 0:
|
|
87
|
+
raise ValueError('sensitivity <= 0')
|
|
88
|
+
self.polling = polling
|
|
89
|
+
if self.polling < 0:
|
|
90
|
+
raise ValueError('polling < 0')
|
|
91
|
+
if max_temp < min_temp:
|
|
92
|
+
raise ValueError('max_temp < min_temp')
|
|
93
|
+
self.min_temp = min_temp
|
|
94
|
+
self.max_temp = max_temp
|
|
95
|
+
if max_level < min_level:
|
|
96
|
+
raise ValueError('max_level < min_level')
|
|
97
|
+
self.min_level = min_level
|
|
98
|
+
self.max_level = max_level
|
|
99
|
+
|
|
100
|
+
# Set the proper temperature function.
|
|
101
|
+
if self.count == 1:
|
|
102
|
+
self.get_temp_func = self.get_1_temp
|
|
103
|
+
else:
|
|
104
|
+
self.get_temp_func = self.get_avg_temp
|
|
105
|
+
if self.temp_calc == self.CALC_MIN:
|
|
106
|
+
self.get_temp_func = self.get_min_temp
|
|
107
|
+
elif self.temp_calc == self.CALC_MAX:
|
|
108
|
+
self.get_temp_func = self.get_max_temp
|
|
109
|
+
|
|
110
|
+
# Try to read device temperature (at this point hwmon_path[] list must be prepared by a child class).
|
|
111
|
+
# If there is any problem with reading temperature, the program will stop here with an exception.
|
|
112
|
+
self.get_temp_func()
|
|
113
|
+
|
|
114
|
+
# Initialize calculated values.
|
|
115
|
+
self.temp_step = (max_temp - min_temp) / steps
|
|
116
|
+
self.level_step = (max_level - min_level) / steps
|
|
117
|
+
self.last_temp = 0
|
|
118
|
+
self.last_level = 0
|
|
119
|
+
self.last_time = time.monotonic() - (polling + 1)
|
|
120
|
+
# Print configuration at DEBUG log level.
|
|
121
|
+
if self.log.log_level >= Log.LOG_CONFIG:
|
|
122
|
+
self.log.msg(Log.LOG_CONFIG, f'{self.name} fan controller was initialized with:')
|
|
123
|
+
self.log.msg(Log.LOG_CONFIG, f' ipmi zone = {self.ipmi_zone}')
|
|
124
|
+
self.log.msg(Log.LOG_CONFIG, f' count = {self.count}')
|
|
125
|
+
self.log.msg(Log.LOG_CONFIG, f' temp_calc = {self.temp_calc}')
|
|
126
|
+
self.log.msg(Log.LOG_CONFIG, f' steps = {self.steps}')
|
|
127
|
+
self.log.msg(Log.LOG_CONFIG, f' sensitivity = {self.sensitivity}')
|
|
128
|
+
self.log.msg(Log.LOG_CONFIG, f' polling = {self.polling}')
|
|
129
|
+
self.log.msg(Log.LOG_CONFIG, f' min_temp = {self.min_temp}')
|
|
130
|
+
self.log.msg(Log.LOG_CONFIG, f' max_temp = {self.max_temp}')
|
|
131
|
+
self.log.msg(Log.LOG_CONFIG, f' min_level = {self.min_level}')
|
|
132
|
+
self.log.msg(Log.LOG_CONFIG, f' max_level = {self.max_level}')
|
|
133
|
+
result=[]
|
|
134
|
+
if hasattr(self, 'hwmon_path'):
|
|
135
|
+
for d in self.hwmon_path:
|
|
136
|
+
result.append(d if d else 'smartctl')
|
|
137
|
+
self.log.msg(Log.LOG_CONFIG, f' hwmon_path = {result}')
|
|
138
|
+
self.print_temp_level_mapping()
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def get_hwmon_path(udevc: Context, parent_dev: Device) -> str:
|
|
142
|
+
"""A helper function to get HWMON path of a given parent device's associated hwmon
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
udevc (Context): pyudev Context
|
|
146
|
+
parent_dev (Device): parent device
|
|
147
|
+
Returns:
|
|
148
|
+
str: path for a HWMON device
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
[hwmon_device] = udevc.list_devices(subsystem='hwmon', parent=parent_dev)
|
|
152
|
+
except ValueError:
|
|
153
|
+
# If parent_dev has zero (or more?) hwmon device in its subtree
|
|
154
|
+
hwmon_device = None
|
|
155
|
+
return os.path.join(hwmon_device.sys_path, 'temp1_input') if hwmon_device is not None else ''
|
|
156
|
+
|
|
157
|
+
def _get_nth_temp(self, index: int) -> float:
|
|
158
|
+
"""Get the temperature of the 'nth' element in the hwmon list. This is an empty implementation."""
|
|
159
|
+
|
|
160
|
+
def get_1_temp(self) -> float:
|
|
161
|
+
"""Get a single temperature of a controlled entity in the IPMI zone.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
float: single temperature of a controlled entity (C)
|
|
165
|
+
"""
|
|
166
|
+
return self._get_nth_temp(0)
|
|
167
|
+
|
|
168
|
+
def get_min_temp(self) -> float:
|
|
169
|
+
"""Get the minimum temperature of multiple controlled entities.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
float: minimum temperature of the controlled entities (C)
|
|
173
|
+
"""
|
|
174
|
+
minimum: float # Minimum temperature value
|
|
175
|
+
|
|
176
|
+
# Calculate minimum temperature.
|
|
177
|
+
minimum = 1000.0
|
|
178
|
+
for i in range(self.count):
|
|
179
|
+
minimum = min(self._get_nth_temp(i), minimum)
|
|
180
|
+
return minimum
|
|
181
|
+
|
|
182
|
+
def get_avg_temp(self):
|
|
183
|
+
"""Get the average temperature of the controlled entities in the IPMI zone.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
float: average temperature of the controlled entities (C)
|
|
187
|
+
"""
|
|
188
|
+
average: float # Average temperature
|
|
189
|
+
counter: int # Value counter
|
|
190
|
+
|
|
191
|
+
# Calculate average temperature.
|
|
192
|
+
average = 0.0
|
|
193
|
+
counter = 0
|
|
194
|
+
for i in range(self.count):
|
|
195
|
+
average += self._get_nth_temp(i)
|
|
196
|
+
counter += 1
|
|
197
|
+
return average / counter
|
|
198
|
+
|
|
199
|
+
def get_max_temp(self) -> float:
|
|
200
|
+
"""Get the maximum temperature of the controlled entities in the IPMI zone.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
float: maximum temperature of the controlled entities (C)
|
|
204
|
+
"""
|
|
205
|
+
maximum: float # Maximum temperature value
|
|
206
|
+
|
|
207
|
+
# Calculate minimum temperature.
|
|
208
|
+
maximum = -1.0
|
|
209
|
+
for i in range(self.count):
|
|
210
|
+
maximum = max(self._get_nth_temp(i), maximum)
|
|
211
|
+
return maximum
|
|
212
|
+
|
|
213
|
+
def set_fan_level(self, level: int) -> None:
|
|
214
|
+
"""Set the new fan level in the IPMI zone of the controller.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
level (int): new fan level [0..100]
|
|
218
|
+
"""
|
|
219
|
+
self.ipmi.set_fan_level(self.ipmi_zone, level)
|
|
220
|
+
|
|
221
|
+
def callback_func(self) -> None:
|
|
222
|
+
"""Call-back function for a child class."""
|
|
223
|
+
|
|
224
|
+
def run(self) -> None:
|
|
225
|
+
"""Run IPMI zone controller function with the following steps:
|
|
226
|
+
|
|
227
|
+
* Step 1: Read current time. If the elapsed time is bigger than the polling time period
|
|
228
|
+
then go to step 2, otherwise return.
|
|
229
|
+
* Step 2: Read the current temperature. If the change of the temperature goes beyond
|
|
230
|
+
the sensitivity limit then go to step 3, otherwise return
|
|
231
|
+
* Step 3: Calculate the current gain and fan level based on the measured temperature
|
|
232
|
+
* Step 4: If the new fan level is different it will be set and logged
|
|
233
|
+
"""
|
|
234
|
+
current_time: float # Current system timestamp (measured)
|
|
235
|
+
current_temp: float # Current temperature (measured)
|
|
236
|
+
current_level: int # Current fan level (calculated)
|
|
237
|
+
current_gain: int # Current gain (calculated)
|
|
238
|
+
|
|
239
|
+
# Step 1: check the elapsed time.
|
|
240
|
+
current_time = time.monotonic()
|
|
241
|
+
if (current_time - self.last_time) >= self.polling:
|
|
242
|
+
self.last_time = current_time
|
|
243
|
+
|
|
244
|
+
# Step 2: read temperature and sensitivity gap.
|
|
245
|
+
self.callback_func()
|
|
246
|
+
current_temp = self.get_temp_func()
|
|
247
|
+
self.log.msg(Log.LOG_DEBUG, f'{self.name}: new temperature > {current_temp:.1f}C')
|
|
248
|
+
if abs(current_temp - self.last_temp) >= self.sensitivity:
|
|
249
|
+
self.last_temp = current_temp
|
|
250
|
+
|
|
251
|
+
# Step 3: calculate gain and fan level.
|
|
252
|
+
if current_temp <= self.min_temp:
|
|
253
|
+
current_level = self.min_level
|
|
254
|
+
elif current_temp >= self.max_temp:
|
|
255
|
+
current_level = self.max_level
|
|
256
|
+
else:
|
|
257
|
+
current_gain = int(round((current_temp - self.min_temp) / self.temp_step))
|
|
258
|
+
current_level = int(round(float(current_gain) * self.level_step)) + self.min_level
|
|
259
|
+
|
|
260
|
+
# Step 4: the new fan level will be set and logged.
|
|
261
|
+
if current_level != self.last_level:
|
|
262
|
+
self.last_level = current_level
|
|
263
|
+
self.set_fan_level(current_level)
|
|
264
|
+
self.log.msg(Log.LOG_INFO, f'{self.name}: new fan level > {current_level}%/{current_temp:.1f}C')
|
|
265
|
+
|
|
266
|
+
def print_temp_level_mapping(self) -> None:
|
|
267
|
+
"""Print out the user-defined temperature to level mapping value in log DEBUG level."""
|
|
268
|
+
self.log.msg(Log.LOG_CONFIG, ' User-defined control function:')
|
|
269
|
+
for i in range(self.steps + 1):
|
|
270
|
+
self.log.msg(Log.LOG_CONFIG, f' {i}. [T:{self.min_temp+(i*self.temp_step):.1f}C - '
|
|
271
|
+
f'L:{int(self.min_level + (i * self.level_step))}%]')
|
|
272
|
+
|
|
273
|
+
# End.
|