3 Commits
1.5.1 ... 1.6.1

Author SHA1 Message Date
0800a3e2e8 reformat with black;
use pwm_min of 1 since newer kernels have different behaviour on pwm=0
2023-08-26 00:15:09 +02:00
6632f364ff added python type hint stub 2021-04-29 09:27:41 +02:00
098807db09 added fan interval per thermalzone and pid interval global 2021-04-29 09:27:14 +02:00
3 changed files with 147 additions and 30 deletions

View File

@@ -6,8 +6,10 @@
# otherwise INFO is a good value.
loglevel: DEBUG
# Set interval (seconds) in which PyFan is checking your temp sources and adjusts PWM values.
interval: 1
# Frequency in seconds of feeding sensors values into PID. Higher frequency (lower value) means PID can react faster.
# Fans only get set every thermalzones->interval seconds, regardless of this value.
# Must be lower then your lowest thermalzone interval.
pid_interval: 0.2
thermalzones:
- name: GPU+SYSTEM
@@ -19,6 +21,11 @@ thermalzones:
# If not, check what you sensor is outputting if you try to read from it.
factor: 1000
# Frequency in which fan speed should be adjusted in seconds.
# PyFan reads sensors values from hwmon every pid_interval, but only sets fan speeds in this interval.
# Can't be lower then pid_interval.
interval: 3
# Define all fans that this thermal zone is going to control.
# There is no limit to how many fans one thermal zone can control.
# You can have different limitations on your fans, for example:
@@ -50,6 +57,7 @@ thermalzones:
- coretemp/temp1_input
- amdgpu/temp1_input
factor: 1000
interval: 3
fan:
- it8686/pwm1: [ 100, 200 ]
target: 50

111
pyfan.py
View File

@@ -15,18 +15,42 @@ class ThermalZone:
def __init__(self, config, pyfan_parent) -> None:
self.fans = config["fan"]
self.temp_source = config["source"]
self.pid = PID(config["pid"]["p"], config["pid"]["i"], config["pid"]["d"], setpoint=0)
self.pid.output_limits = (0, 255)
self.factor = 1 / config["factor"]
self.name = config["name"]
self.target = config["target"]
self.hwmap = pyfan_parent.hwmap
self.pyfan = pyfan_parent
self.hwmap = self.pyfan.hwmap
self.alias_replace = re.compile("|".join(self.hwmap.keys()))
if "interval" not in config:
config["interval"] = 3
logging.getLogger("pyfan").warning(
"[%s] No interval specified, using default. This is deprecated since 1.6 and may be removed in future "
"versions. See example config for reference.",
self.name,
)
self.pid = PID(
config["pid"]["p"],
config["pid"]["i"],
config["pid"]["d"],
setpoint=0,
sample_time=config["interval"],
)
# we prefer 3 for pwm_enable, but since some chips do not support this mode, use 1 with pwm_min of 1 as fallback
# more on pwm=0 behaviour: https://www.kernel.org/doc/html/latest/hwmon/pwm-fan.html
self.pid.output_limits = (1, 255)
self.setup_pwm()
logging.getLogger("pyfan").info("[%s] Source=%s Fans=%s Factor=%d PID=%s", self.name, self.temp_source,
self.fans, self.factor, self.pid)
logging.getLogger("pyfan").info(
"[%s] Source=%s Fans=%s Factor=%f %s",
self.name,
self.temp_source,
self.fans,
self.factor,
self.pid,
)
def eval(self):
if self.get_temp():
@@ -42,23 +66,46 @@ class ThermalZone:
if isinstance(fan_val, list):
if len(fan_val) < 2:
logging.getLogger("pyfan").warning(
"[%s] max/min for %s was not set correctly (%s)", self.name, fan, fan_val)
"[%s] max/min for %s was not set correctly (%s)",
self.name,
fan,
fan_val,
)
self.write_sysfs(fan, min(fan_val[1], max(val, fan_val[0])))
else:
if self.read_sysfs(fan) != min(
fan_val[1], max(val, fan_val[0])
):
self.write_sysfs(
fan, min(fan_val[1], max(val, fan_val[0]))
)
elif self.read_sysfs(fan) != min(val, fan_val):
self.write_sysfs(fan, min(val, fan_val))
logging.getLogger("pyfan").debug("[%s] %s=%i%%", self.name, fan,
int(int(self.read_sysfs(fan)) / 255 * 100))
else:
logging.getLogger("pyfan").debug(
"[%s] %s=%i%%",
self.name,
fan,
int(int(self.read_sysfs(fan)) / 255 * 100),
)
elif self.read_sysfs(target_fan) != val:
self.write_sysfs(target_fan, val)
except OSError as err:
logging.getLogger("pyfan").warning("[%s] Failed to set pwm, trying to reset it. (%s)", self.name,
err.strerror)
logging.getLogger("pyfan").warning(
"[%s] Failed to set pwm, trying to reset it. (%s)",
self.name,
err.strerror,
)
self.setup_pwm(1)
logging.getLogger("pyfan").debug("[%s] %i%% D:%iC T:%iC pid:%s", self.name, int(val / 255 * 100), diff,
self.get_temp(), self.pid)
logging.getLogger("pyfan").debug(
"[%s] %i%% D:%iC T:%iC %s",
self.name,
int(val / 255 * 100),
diff,
self.get_temp(),
self.pid,
)
def get_temp(self):
if isinstance(self.temp_source, list):
@@ -85,7 +132,9 @@ class ThermalZone:
else:
self.set_pwm_mode(target_fan, value)
except FileNotFoundError:
logging.getLogger("pyfan").warning("[%s] pwm not found. Try reloading hwmon map...", self.name)
logging.getLogger("pyfan").warning(
"[%s] pwm not found. Try reloading hwmon map...", self.name
)
self.hwmap = self.pyfan.hwmap
def replace_alias(self, path):
@@ -105,8 +154,11 @@ class ThermalZone:
with open(self.build_pwm_path(path)) as sysfs_f:
return sysfs_f.readline()
except FileNotFoundError as err:
logging.getLogger("pyfan").warning("[%s] temp source not found. Not ready yet or wrong path? (%s)",
self.name, err.strerror)
logging.getLogger("pyfan").warning(
"[%s] temp source not found. Not ready yet or wrong path? (%s)",
self.name,
err.strerror,
)
return None
def set_pwm_mode(self, path, value=1):
@@ -118,18 +170,21 @@ class PyFan:
self.config = self.__load_config(config)
logging.basicConfig(level=logging.getLevelName(self.config["loglevel"]))
self.zones = []
self._hwmon_map = {}
if "interval" in self.config:
self.interval = self.config["interval"]
if "pid_interval" not in self.config:
self.interval = 0.2
logging.getLogger("pyfan").warning(
"No pid_interval specified, using default. This is deprecated since 1.6 and may be removed in future "
"versions. See example config for reference."
)
else:
self.interval = 1
self.interval = self.config["pid_interval"]
for zone in self.config["thermalzones"]:
self.zones.append(ThermalZone(zone, self))
logging.getLogger("pyfan").info(
"Finished creating %d thermal zones, checking all %d seconds", len(self.zones), self.interval)
"Created %d thermal zones, pid_interval=%f.", len(self.zones), self.interval
)
def __enter__(self):
return self
@@ -144,16 +199,16 @@ class PyFan:
@property
def hwmap(self):
self._hwmon_map.clear()
hwmon_map = {}
names = glob.glob(SYSFS_HWMON_BASE + "hwmon*/name")
for name in names:
hwmon = name.split("/")[-2]
with open(name) as file:
hwname = file.read().strip()
self._hwmon_map[hwname] = hwmon
return self._hwmon_map
hw_name = file.read().strip()
hwmon_map[hw_name] = hwmon
return hwmon_map
@staticmethod
def __load_config(path):

54
pyfan.pyi Normal file
View File

@@ -0,0 +1,54 @@
from typing import Any, Pattern
from simple_pid import PID
SYSFS_HWMON_BASE: str
class ThermalZone:
fans: Any = ...
temp_source: Any = ...
pid: PID = ...
factor: float = ...
name: str = ...
target: Any = ...
hwmap: dict = ...
pyfan: PyFan = ...
alias_replace: Pattern = ...
def __init__(self, config: dict, pyfan_parent: PyFan) -> None: ...
def eval(self) -> None: ...
def get_temp(self): ...
def restore(self) -> None: ...
def setup_pwm(self, value: int = ...) -> None: ...
def replace_alias(self, path: str): ...
def build_pwm_path(self, specific: Any): ...
def write_sysfs(self, path: str, value: Any) -> None: ...
def read_sysfs(self, path: str): ...
def set_pwm_mode(self, path: str, value: int = ...) -> None: ...
class PyFan:
config: dict = ...
zones: list = ...
interval: float = ...
def __init__(self, config: str = ...) -> None: ...
def __enter__(self): ...
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: ...
def eval(self) -> None: ...
@property
def hwmap(self): ...