Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
8d95e69048 | |||
772408fc6d | |||
e980293ab6 | |||
6e45b77aba | |||
6074353b95 | |||
3f96734d84 | |||
dd9084ef0d | |||
fe99608da6 | |||
89aa3516e1 | |||
30201163f9 | |||
69bac0b97b | |||
e4bda467bf | |||
f8b1d87a4e | |||
01cd1a1517 | |||
87ba120390 | |||
180ff99c4f | |||
67ba3d8fca | |||
0bd909d157 | |||
2011226cac | |||
eb220f744a |
149
.gitignore
vendored
149
.gitignore
vendored
@@ -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
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
@@ -21,6 +126,8 @@ parts/
|
|||||||
sdist/
|
sdist/
|
||||||
var/
|
var/
|
||||||
wheels/
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
.installed.cfg
|
.installed.cfg
|
||||||
*.egg
|
*.egg
|
||||||
@@ -57,6 +164,7 @@ coverage.xml
|
|||||||
*.log
|
*.log
|
||||||
local_settings.py
|
local_settings.py
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
# Flask stuff:
|
# Flask stuff:
|
||||||
instance/
|
instance/
|
||||||
@@ -81,6 +189,13 @@ ipython_config.py
|
|||||||
# pyenv
|
# pyenv
|
||||||
.python-version
|
.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
|
# celery beat schedule file
|
||||||
celerybeat-schedule
|
celerybeat-schedule
|
||||||
|
|
||||||
@@ -114,18 +229,30 @@ dmypy.json
|
|||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.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
|
# Dump file
|
||||||
.fuse_hidden*
|
*.stackdump
|
||||||
|
|
||||||
# KDE directory preferences
|
# Folder config file
|
||||||
.directory
|
[Dd]esktop.ini
|
||||||
|
|
||||||
# Linux trash folder which might appear on any partition or disk
|
# Recycle Bin used on file shares
|
||||||
.Trash-*
|
$RECYCLE.BIN/
|
||||||
|
|
||||||
# .nfs files are created when an open file is removed but is still being accessed
|
# Windows Installer files
|
||||||
.nfs*
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msix
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
|
||||||
|
# Windows shortcuts
|
||||||
|
*.lnk
|
||||||
|
|
||||||
|
# End of https://www.gitignore.io/api/linux,python,windows,pycharm+all
|
@@ -1,12 +1,14 @@
|
|||||||
## PyFAN
|
## 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
|
# 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:
|
To know which hwmon is what device and what pwm controls what fan, the following commands can help you:
|
||||||
|
|
||||||
|
@@ -1,26 +1,25 @@
|
|||||||
loglevel: DEBUG
|
loglevel: DEBUG
|
||||||
|
interval: 1
|
||||||
|
|
||||||
thermalzones:
|
thermalzones:
|
||||||
-
|
- name: GPU+SYSTEM
|
||||||
name: GPU+SYSTEM
|
source: amdgpu/temp1_input
|
||||||
source: hwmon3/temp1_input
|
|
||||||
factor: 1000
|
factor: 1000
|
||||||
fan:
|
fan:
|
||||||
- hwmon2/pwm2
|
- it8686/pwm2
|
||||||
- hwmon3/pwm1: 170
|
- amdgpu/pwm1: 170
|
||||||
target: 60
|
target: 60
|
||||||
pid:
|
pid:
|
||||||
p: 1
|
p: 1
|
||||||
i: 1.5
|
i: 1
|
||||||
d: 2
|
d: 1.5
|
||||||
-
|
- name: CPU
|
||||||
name: CPU
|
source: coretemp/temp1_input
|
||||||
source: hwmon1/temp1_input
|
|
||||||
factor: 1000
|
factor: 1000
|
||||||
fan:
|
fan:
|
||||||
- hwmon2/pwm1
|
- it8686/pwm1: [100, 200]
|
||||||
target: 50
|
target: 50
|
||||||
pid:
|
pid:
|
||||||
p: 1
|
p: 1
|
||||||
i: 1.5
|
i: 1
|
||||||
d: 2
|
d: 1.5
|
158
pyfan.py
158
pyfan.py
@@ -1,94 +1,139 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
import glob
|
||||||
import sys
|
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from simple_pid import PID
|
from simple_pid import PID
|
||||||
|
|
||||||
SYSFS_HWMON_BASE = "/sys/class/hwmon/"
|
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:
|
class ThermalZone:
|
||||||
def __init__(self, temp_source, fans, p, i, d, target, factor, name) -> None:
|
def __init__(self, config, hwmon_map) -> None:
|
||||||
self.fans = fans
|
self.fans = config["fan"]
|
||||||
self.temp_source = temp_source
|
self.temp_source = config["source"]
|
||||||
self.pid = PID(p, i, d, setpoint=0)
|
self.pid = PID(config["pid"]["p"], config["pid"]["i"], config["pid"]["d"], setpoint=0)
|
||||||
self.pid.output_limits = (0, 255)
|
self.pid.output_limits = (0, 255)
|
||||||
self.factor = 1 / factor
|
self.factor = 1 / config["factor"]
|
||||||
self.name = name
|
self.name = config["name"]
|
||||||
self.target = target
|
self.target = config["target"]
|
||||||
|
self.hwmap = hwmon_map
|
||||||
|
self.alias_replace = re.compile('|'.join(self.hwmap.keys()))
|
||||||
self.setup_pwm()
|
self.setup_pwm()
|
||||||
|
|
||||||
logging.info("[{zone}] Source={source} Fans={fans} Factor={factor} PID={pid}".format(zone=name,
|
logging.getLogger("pyfan").info(
|
||||||
source=temp_source,
|
"[{zone}] Source={source} Fans={fans} Factor={factor} PID={pid}".format(zone=self.name,
|
||||||
fans=fans,
|
source=self.temp_source,
|
||||||
factor=factor,
|
fans=self.fans,
|
||||||
|
factor=self.factor,
|
||||||
pid=(
|
pid=(
|
||||||
p, i, d)))
|
self.pid.Kp, self.pid.Ki,
|
||||||
|
self.pid.Kd)))
|
||||||
|
|
||||||
def eval(self):
|
def eval(self):
|
||||||
|
if self.get_temp():
|
||||||
diff = self.target - 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:
|
for target_fan in self.fans:
|
||||||
if type(target_fan) is dict:
|
if type(target_fan) is dict:
|
||||||
write_sysfs(list(target_fan.keys())[0], min(int(val), list(target_fan.values())[0]))
|
fan = list(target_fan.keys())[0]
|
||||||
else:
|
fan_val = list(target_fan.values())[0]
|
||||||
write_sysfs(target_fan, int(val))
|
|
||||||
|
|
||||||
logging.debug(
|
if type(fan_val) is list:
|
||||||
"[{name}] {val}% ({diff}C/{temp}C)".format(name=self.name, val=int(val / 255 * 100), diff=diff,
|
if len(fan_val) < 2:
|
||||||
temp=self.get_temp()))
|
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):
|
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):
|
def restore(self):
|
||||||
self.setup_pwm(2)
|
self.setup_pwm(2)
|
||||||
|
|
||||||
def setup_pwm(self, value=1):
|
def setup_pwm(self, value=1):
|
||||||
for target_fan in self.fans:
|
for target_fan in self.fans:
|
||||||
|
try:
|
||||||
if type(target_fan) is dict:
|
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:
|
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:
|
class PyFan:
|
||||||
def __init__(self, config="/etc/pyfan") -> None:
|
def __init__(self, config="/etc/pyfan") -> None:
|
||||||
self.config = self.__load_config(config)
|
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.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"]:
|
for zone in self.config["thermalzones"]:
|
||||||
self.zones.append(
|
self.zones.append(ThermalZone(zone, self.hwmon_map))
|
||||||
ThermalZone(zone["source"], zone["fan"], zone["pid"]["p"], zone["pid"]["i"], zone["pid"]["d"],
|
|
||||||
zone["target"],
|
|
||||||
zone["factor"], zone["name"]))
|
|
||||||
|
|
||||||
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):
|
def __enter__(self):
|
||||||
return self
|
return self
|
||||||
@@ -106,12 +151,21 @@ class PyFan:
|
|||||||
with open(path) as cfg_file:
|
with open(path) as cfg_file:
|
||||||
return yaml.safe_load(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__":
|
if __name__ == "__main__":
|
||||||
with PyFan() as pyfan:
|
with PyFan() as pyfan:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
pyfan.eval()
|
pyfan.eval()
|
||||||
sleep(1)
|
sleep(pyfan.interval)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Start PyFan fan control
|
Description=PID based fan control
|
||||||
ConditionFileNotEmpty=/etc/pyfan
|
ConditionFileNotEmpty=/etc/pyfan
|
||||||
After=lm_sensors.service
|
After=lm_sensors.service
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user