20 Commits
1.0 ... 1.3

Author SHA1 Message Date
8d95e69048 added more debug log to determine current pwm setting 2019-12-27 15:11:19 +01:00
772408fc6d fixed interval key check 2019-12-27 14:48:16 +01:00
e980293ab6 added interval and min/max config options
added [min, max] option to fans
added pid check interval setting to daemon
2019-12-27 14:30:38 +01:00
6e45b77aba Update 'README.md' 2019-10-04 11:26:12 +02:00
6074353b95 Merge remote-tracking branch 'origin/master' 2019-10-04 11:13:27 +02:00
3f96734d84 removed unused config variable 2019-10-04 11:13:19 +02:00
dd9084ef0d Switched back to sbin 2019-10-04 10:23:47 +02:00
fe99608da6 Switched sbin to bin 2019-10-04 10:21:27 +02:00
89aa3516e1 fixed description 2019-10-04 10:12:59 +02:00
30201163f9 Adjusted service file path 2019-10-04 10:12:27 +02:00
69bac0b97b handled None return for temp 2019-09-15 00:47:09 +02:00
e4bda467bf fixed file not found when hwmon was not fully initialized on startup 2019-09-15 00:43:46 +02:00
f8b1d87a4e adjusted .service file to better suite manual installations 2019-09-11 15:21:14 +02:00
01cd1a1517 made .gitignore include idea 2019-09-11 15:19:13 +02:00
87ba120390 Update 'README.md' 2019-09-11 15:08:57 +02:00
180ff99c4f fixed dict containing newlines
added missing alias in config
2019-09-11 15:00:39 +02:00
67ba3d8fca added hwmon alias map
linux kernel does not guarantee order of hwmon devices, so use alias instead that gets replaced with teh correct hwmonX, based on hwmonX/name
2019-09-11 14:43:49 +02:00
0bd909d157 fixed reset warning
added handling for file not found
2019-09-08 21:53:26 +02:00
2011226cac improved warning logging 2019-09-06 10:00:21 +02:00
eb220f744a better error handling
fixed resume reset
better logging messages

Signed-off-by: Giovanni Harting <539@idlegandalf.com>
2019-09-06 07:23:59 +02:00
5 changed files with 278 additions and 96 deletions

149
.gitignore vendored
View File

@@ -1,4 +1,109 @@
# ---> Python
# Created by https://www.gitignore.io/api/linux,python,windows,pycharm+all
# Edit at https://www.gitignore.io/?templates=linux,python,windows,pycharm+all
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### PyCharm+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### PyCharm+all Patch ###
# Ignores the whole .idea folder and all .iml files
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
.idea/
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
# Sonarlint plugin
.idea/sonarlint
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@@ -21,6 +126,8 @@ parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
@@ -57,6 +164,7 @@ coverage.xml
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
@@ -81,6 +189,13 @@ ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# celery beat schedule file
celerybeat-schedule
@@ -114,18 +229,30 @@ dmypy.json
# Pyre type checker
.pyre/
# ---> Linux
*~
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# Dump file
*.stackdump
# KDE directory preferences
.directory
# Folder config file
[Dd]esktop.ini
# Linux trash folder which might appear on any partition or disk
.Trash-*
# Recycle Bin used on file shares
$RECYCLE.BIN/
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.gitignore.io/api/linux,python,windows,pycharm+all

View File

@@ -1,12 +1,14 @@
## PyFAN
This python script utilizes hwmon and PID as base for fan controlling.
This python script utilizes linux's hwmon interface and a PID controller for fan controlling. No external dependencies besides simple_pid and PyYAML are needed.
# Usage
Put your config in /etc/pyfan (reference example config) and enable pyfan as a service (service file also available here).
Put your config in /etc/pyfan (see example config) and enable pyfan as a service (see example service file).
# Finding your fans
> It is recommended to use pwmconfig from lm_sensors to detect your fans.
# Config
To know which hwmon is what device and what pwm controls what fan, the following commands can help you:

View File

@@ -1,26 +1,25 @@
loglevel: DEBUG
interval: 1
thermalzones:
-
name: GPU+SYSTEM
source: hwmon3/temp1_input
- name: GPU+SYSTEM
source: amdgpu/temp1_input
factor: 1000
fan:
- hwmon2/pwm2
- hwmon3/pwm1: 170
- it8686/pwm2
- amdgpu/pwm1: 170
target: 60
pid:
p: 1
i: 1.5
d: 2
-
name: CPU
source: hwmon1/temp1_input
i: 1
d: 1.5
- name: CPU
source: coretemp/temp1_input
factor: 1000
fan:
- hwmon2/pwm1
- it8686/pwm1: [100, 200]
target: 50
pid:
p: 1
i: 1.5
d: 2
i: 1
d: 1.5

158
pyfan.py
View File

@@ -1,94 +1,139 @@
#!/usr/bin/env python
import sys
import glob
import logging
import re
import sys
from time import sleep
import yaml
from simple_pid import PID
SYSFS_HWMON_BASE = "/sys/class/hwmon/"
def build_pwm_path(specific):
return SYSFS_HWMON_BASE + specific
def write_sysfs(path, value):
try:
with open(build_pwm_path(path), 'w') as sysfs_f:
sysfs_f.write(str(value))
except OSError as err:
print("WARN:", err.strerror)
def read_sysfs(path):
with open(build_pwm_path(path)) as sysfs_f:
return sysfs_f.readline()
def set_pwm_mode(path, value=1):
write_sysfs(path + "_enable", value)
class ThermalZone:
def __init__(self, temp_source, fans, p, i, d, target, factor, name) -> None:
self.fans = fans
self.temp_source = temp_source
self.pid = PID(p, i, d, setpoint=0)
def __init__(self, config, hwmon_map) -> 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 / factor
self.name = name
self.target = target
self.factor = 1 / config["factor"]
self.name = config["name"]
self.target = config["target"]
self.hwmap = hwmon_map
self.alias_replace = re.compile('|'.join(self.hwmap.keys()))
self.setup_pwm()
logging.info("[{zone}] Source={source} Fans={fans} Factor={factor} PID={pid}".format(zone=name,
source=temp_source,
fans=fans,
factor=factor,
logging.getLogger("pyfan").info(
"[{zone}] Source={source} Fans={fans} Factor={factor} PID={pid}".format(zone=self.name,
source=self.temp_source,
fans=self.fans,
factor=self.factor,
pid=(
p, i, d)))
self.pid.Kp, self.pid.Ki,
self.pid.Kd)))
def eval(self):
if self.get_temp():
diff = self.target - self.get_temp()
val = self.pid(diff)
val = int(self.pid(diff))
try:
for target_fan in self.fans:
if type(target_fan) is dict:
write_sysfs(list(target_fan.keys())[0], min(int(val), list(target_fan.values())[0]))
else:
write_sysfs(target_fan, int(val))
fan = list(target_fan.keys())[0]
fan_val = list(target_fan.values())[0]
logging.debug(
"[{name}] {val}% ({diff}C/{temp}C)".format(name=self.name, val=int(val / 255 * 100), diff=diff,
temp=self.get_temp()))
if type(fan_val) is 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))
self.write_sysfs(fan, min(fan_val[1], max(val, fan_val[0])))
else:
self.write_sysfs(fan, min(val, fan_val))
logging.getLogger("pyfan").debug(
"[{name}] {fan} is set at {val}%".format(name=self.name, fan=fan,
val=int(int(self.read_sysfs(fan)) / 255 * 100)))
else:
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))
self.setup_pwm(1)
p, i, d = self.pid.components
logging.getLogger("pyfan").debug(
"[{name}] {val}% ({diff}C/{temp}C) ({p}|{i}|{d})".format(name=self.name, val=int(val / 255 * 100),
diff=diff,
temp=self.get_temp(), p=int(p), i=int(i),
d=int(d)))
def get_temp(self):
return float(read_sysfs(self.temp_source)) * self.factor
if self.read_sysfs(self.temp_source):
return float(self.read_sysfs(self.temp_source)) * self.factor
else:
return None
def restore(self):
self.setup_pwm(2)
def setup_pwm(self, value=1):
for target_fan in self.fans:
try:
if type(target_fan) is dict:
set_pwm_mode(list(target_fan.keys())[0], value)
self.set_pwm_mode(list(target_fan.keys())[0], value)
else:
set_pwm_mode(target_fan, value)
self.set_pwm_mode(target_fan, value)
except FileNotFoundError as err:
logging.getLogger("pyfan").warning("[%s] pwm not found."
" Not ready yet or wrong path? (%s)" % (self.name, err.strerror))
def replace_alias(self, path):
replaced = self.alias_replace.sub(lambda x: self.hwmap[x.group()], path)
logging.getLogger("pyfan").debug("[ALIAS] %s -> %s" % (path, replaced))
return replaced
def build_pwm_path(self, specific):
return self.replace_alias(SYSFS_HWMON_BASE + specific)
def write_sysfs(self, path, value):
with open(self.build_pwm_path(path), 'w') as sysfs_f:
sysfs_f.write(str(value))
def read_sysfs(self, path):
try:
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))
return None
def set_pwm_mode(self, path, value=1):
self.write_sysfs(path + "_enable", value)
class PyFan:
def __init__(self, config="/etc/pyfan") -> None:
self.config = self.__load_config(config)
logging.basicConfig(level=logging.getLevelName(self.config["loglevel"]), style='{')
logging.basicConfig(level=logging.getLevelName(self.config["loglevel"]))
self.zones = []
self.hwmon_map = {}
self.gen_hwmon_map()
if "interval" in self.config:
self.interval = self.config["interval"]
else:
self.interval = 1
for zone in self.config["thermalzones"]:
self.zones.append(
ThermalZone(zone["source"], zone["fan"], zone["pid"]["p"], zone["pid"]["i"], zone["pid"]["d"],
zone["target"],
zone["factor"], zone["name"]))
self.zones.append(ThermalZone(zone, self.hwmon_map))
logging.info("Finished creating %d thermal zones." % len(self.zones))
logging.getLogger("pyfan").info("Finished creating %d thermal zones." % len(self.zones))
def __enter__(self):
return self
@@ -106,12 +151,21 @@ class PyFan:
with open(path) as cfg_file:
return yaml.safe_load(cfg_file)
def gen_hwmon_map(self):
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
if __name__ == "__main__":
with PyFan() as pyfan:
while True:
try:
pyfan.eval()
sleep(1)
sleep(pyfan.interval)
except KeyboardInterrupt:
sys.exit(0)

View File

@@ -1,5 +1,5 @@
[Unit]
Description=Start PyFan fan control
Description=PID based fan control
ConditionFileNotEmpty=/etc/pyfan
After=lm_sensors.service