Add test mode
This commit is contained in:
@@ -69,7 +69,10 @@ Before using HDD Fan control unmonitored for long period of time, I recommend k
|
|||||||
|
|
||||||
### Fan configuration
|
### Fan configuration
|
||||||
|
|
||||||
To get the value for the `--pwm`, `--pwm-start-value` and `--pwm-stop-value` parameters, use the [pwmconfig tool](http://www.lm-sensors.org/wiki/man/pwmconfig).
|
To get the value for the `--pwm`, `--pwm-start-value` and `--pwm-stop-value` parameters, you can either:
|
||||||
|
|
||||||
|
* Use the `-t` or `--test` parameter, which will run some tests and detect the values at which the fans start and stop. However you need to have previously identified the PWM file (the `--pwm` parameter)
|
||||||
|
* use the [pwmconfig tool](http://www.lm-sensors.org/wiki/man/pwmconfig).
|
||||||
|
|
||||||
### Drive auto spin down
|
### Drive auto spin down
|
||||||
|
|
||||||
@@ -91,16 +94,15 @@ Some Hitachi (now HGST) drives support a special way of querying temperature tha
|
|||||||
|
|
||||||
usage: hdd_fancontrol.py [-h] -d DRIVE_FILEPATHS [DRIVE_FILEPATHS ...] -p
|
usage: hdd_fancontrol.py [-h] -d DRIVE_FILEPATHS [DRIVE_FILEPATHS ...] -p
|
||||||
FAN_PWM_FILEPATH [FAN_PWM_FILEPATH ...]
|
FAN_PWM_FILEPATH [FAN_PWM_FILEPATH ...]
|
||||||
--pwm-start-value FAN_START_VALUE
|
[--pwm-start-value FAN_START_VALUE [FAN_START_VALUE ...]]
|
||||||
[FAN_START_VALUE ...] --pwm-stop-value FAN_STOP_VALUE
|
[--pwm-stop-value FAN_STOP_VALUE [FAN_STOP_VALUE ...]]
|
||||||
[FAN_STOP_VALUE ...] [--min-temp MIN_TEMP]
|
[--min-temp MIN_TEMP] [--max-temp MAX_TEMP]
|
||||||
[--max-temp MAX_TEMP]
|
|
||||||
[--min-fan-speed-prct MIN_FAN_SPEED_PRCT]
|
[--min-fan-speed-prct MIN_FAN_SPEED_PRCT]
|
||||||
[-i INTERVAL_S] --stat-files STAT_FILEPATHS
|
[-i INTERVAL_S] --stat-files STAT_FILEPATHS
|
||||||
[STAT_FILEPATHS ...]
|
[STAT_FILEPATHS ...]
|
||||||
[--spin-down-time SPIN_DOWN_TIME_S]
|
[--spin-down-time SPIN_DOWN_TIME_S]
|
||||||
[-v {warning,normal,debug}] [-b] [-l LOG_FILEPATH]
|
[-v {warning,normal,debug}] [-b] [-l LOG_FILEPATH]
|
||||||
[--pid-file PID_FILEPATH]
|
[--pid-file PID_FILEPATH] [-t]
|
||||||
|
|
||||||
Dynamically control fan speed according to hard drive temperature.
|
Dynamically control fan speed according to hard drive temperature.
|
||||||
|
|
||||||
@@ -111,12 +113,12 @@ Some Hitachi (now HGST) drives support a special way of querying temperature tha
|
|||||||
-p FAN_PWM_FILEPATH [FAN_PWM_FILEPATH ...], --pwm FAN_PWM_FILEPATH [FAN_PWM_FILEPATH ...]
|
-p FAN_PWM_FILEPATH [FAN_PWM_FILEPATH ...], --pwm FAN_PWM_FILEPATH [FAN_PWM_FILEPATH ...]
|
||||||
PWM filepath(s) to control fan speed (under /sys)
|
PWM filepath(s) to control fan speed (under /sys)
|
||||||
--pwm-start-value FAN_START_VALUE [FAN_START_VALUE ...]
|
--pwm-start-value FAN_START_VALUE [FAN_START_VALUE ...]
|
||||||
PWM value (0-255), at which the fan starts moving. Run
|
PWM value (0-255), at which the fan starts moving. Use
|
||||||
pwmconfig to find this value.
|
the -t parameter, or run pwmconfig to find this value.
|
||||||
--pwm-stop-value FAN_STOP_VALUE [FAN_STOP_VALUE ...]
|
--pwm-stop-value FAN_STOP_VALUE [FAN_STOP_VALUE ...]
|
||||||
PWM value (0-255), at which the fan stop moving. Run
|
PWM value (0-255), at which the fan stop moving. Use
|
||||||
pwmconfig to find this value. Often 20-40 lower than
|
the -t parameter, or run pwmconfig to find this value.
|
||||||
start speed.
|
Often 20-40 lower than start speed.
|
||||||
--min-temp MIN_TEMP Temperature in Celcius at which the fan(s) will be set
|
--min-temp MIN_TEMP Temperature in Celcius at which the fan(s) will be set
|
||||||
to minimum speed.
|
to minimum speed.
|
||||||
--max-temp MAX_TEMP Temperature in Celcius at which the fan(s) will be set
|
--max-temp MAX_TEMP Temperature in Celcius at which the fan(s) will be set
|
||||||
@@ -141,6 +143,7 @@ Some Hitachi (now HGST) drives support a special way of querying temperature tha
|
|||||||
Filepath for log output when using deamon mode
|
Filepath for log output when using deamon mode
|
||||||
--pid-file PID_FILEPATH
|
--pid-file PID_FILEPATH
|
||||||
Filepath for lock file when using deamon mode
|
Filepath for lock file when using deamon mode
|
||||||
|
-t, --test Run some tests and exit
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
+199
-27
@@ -10,6 +10,7 @@ import logging
|
|||||||
import operator
|
import operator
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import stat
|
import stat
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
@@ -175,6 +176,23 @@ class Fan:
|
|||||||
""" Return True if fan is moving, False instead. """
|
""" Return True if fan is moving, False instead. """
|
||||||
return (self.getRpm() > 0)
|
return (self.getRpm() > 0)
|
||||||
|
|
||||||
|
def waitStabilize(self):
|
||||||
|
"""
|
||||||
|
Wait for the fan to have a stable rotational speed.
|
||||||
|
|
||||||
|
The algorithm only works if the fan is either slowing down, accelerating, or steady during the test, not if its speed
|
||||||
|
changes quickly ie. going up and down.
|
||||||
|
"""
|
||||||
|
rpm = self.getRpm()
|
||||||
|
min_rpm, max_rpm = rpm, rpm
|
||||||
|
while True:
|
||||||
|
time.sleep(2)
|
||||||
|
rpm = self.getRpm()
|
||||||
|
if min_rpm <= rpm <= max_rpm:
|
||||||
|
break
|
||||||
|
min_rpm = min(min_rpm, rpm)
|
||||||
|
max_rpm = max(max_rpm, rpm)
|
||||||
|
|
||||||
def setSpeed(self, speed_prct, min_prct):
|
def setSpeed(self, speed_prct, min_prct):
|
||||||
""" Set fan speed to a percentage of its maximum speed. """
|
""" Set fan speed to a percentage of its maximum speed. """
|
||||||
# preconditions
|
# preconditions
|
||||||
@@ -221,6 +239,145 @@ class Fan:
|
|||||||
pwm_file.write("%u" % (value))
|
pwm_file.write("%u" % (value))
|
||||||
|
|
||||||
|
|
||||||
|
class TestHardware:
|
||||||
|
|
||||||
|
""" Run basic drive tests, and analyze fan start/stop behaviour. """
|
||||||
|
|
||||||
|
def __init__(self, drives, fans):
|
||||||
|
self.drives = drives
|
||||||
|
self.fans = fans
|
||||||
|
self.ok_count = 0
|
||||||
|
self.ko_count = 0
|
||||||
|
self.logger = logging.getLogger(__class__.__name__)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.logger.info("Running hardware tests, this may take a few minutes")
|
||||||
|
self.testDrives()
|
||||||
|
start_stop_values = self.testPwms()
|
||||||
|
if self.ko_count > 0:
|
||||||
|
print("%u/%u tests failed!" % (self.ko_count, self.ko_count + self.ok_count))
|
||||||
|
else:
|
||||||
|
print("%u/%u tests OK, all good :)" % (self.ok_count, self.ok_count))
|
||||||
|
self.logger.info("Recommended parameters: --pwm-start-value %s --pwm-stop-value %s"
|
||||||
|
% (" ".join(str(min(255, x[0] + 32)) for x in start_stop_values),
|
||||||
|
" ".join(str(x[1]) for x in start_stop_values)))
|
||||||
|
|
||||||
|
def testDrives(self):
|
||||||
|
for drive in self.drives:
|
||||||
|
self.reportTestGroupStart("Test of drive %s" % (drive))
|
||||||
|
|
||||||
|
test_desc = "Getting drive power state"
|
||||||
|
self.reportTestStart(test_desc)
|
||||||
|
try:
|
||||||
|
state = drive.getState()
|
||||||
|
test_ok = state in Drive.DriveState
|
||||||
|
except:
|
||||||
|
test_ok = False
|
||||||
|
self.reportTestResult(test_desc, test_ok)
|
||||||
|
|
||||||
|
test_desc = "Getting drive temperature"
|
||||||
|
self.reportTestStart(test_desc)
|
||||||
|
try:
|
||||||
|
temp = drive.getTemperature()
|
||||||
|
test_ok = isinstance(temp, int)
|
||||||
|
except:
|
||||||
|
test_ok = False
|
||||||
|
self.reportTestResult(test_desc, test_ok)
|
||||||
|
|
||||||
|
test_desc = "Getting drive activity statistics"
|
||||||
|
self.reportTestStart(test_desc)
|
||||||
|
try:
|
||||||
|
stats = drive.getActivityStats()
|
||||||
|
test_ok = isinstance(stats, tuple)
|
||||||
|
except:
|
||||||
|
test_ok = False
|
||||||
|
self.reportTestResult(test_desc, test_ok)
|
||||||
|
|
||||||
|
def testPwms(self):
|
||||||
|
start_stop_values = []
|
||||||
|
pwm_vals = (255,) + tuple(range(240, -1, -16))
|
||||||
|
for fan in self.fans:
|
||||||
|
self.reportTestGroupStart("Test of fan #%u" % (fan.id))
|
||||||
|
start_value, stop_value = 255, 0
|
||||||
|
|
||||||
|
test_desc = "Stopping fan"
|
||||||
|
self.reportTestStart(test_desc)
|
||||||
|
try:
|
||||||
|
fan.setPwmValue(0)
|
||||||
|
fan.waitStabilize()
|
||||||
|
test_ok = not fan.isRunning()
|
||||||
|
except:
|
||||||
|
test_ok = False
|
||||||
|
self.reportTestResult(test_desc, test_ok)
|
||||||
|
|
||||||
|
test_desc = "Starting fan"
|
||||||
|
self.reportTestStart(test_desc)
|
||||||
|
try:
|
||||||
|
fan.setPwmValue(255)
|
||||||
|
fan.waitStabilize()
|
||||||
|
test_ok = fan.isRunning()
|
||||||
|
except:
|
||||||
|
test_ok = False
|
||||||
|
self.reportTestResult(test_desc, test_ok)
|
||||||
|
|
||||||
|
test_desc = "Finding exact start value of fan"
|
||||||
|
self.reportTestStart(test_desc)
|
||||||
|
test_ok = False
|
||||||
|
try:
|
||||||
|
for v in reversed(pwm_vals):
|
||||||
|
fan.setPwmValue(v)
|
||||||
|
fan.waitStabilize()
|
||||||
|
test_ok = fan.isRunning()
|
||||||
|
if test_ok:
|
||||||
|
start_value = v
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.reportTestResult(test_desc, test_ok)
|
||||||
|
|
||||||
|
test_desc = "Finding exact stop value of fan"
|
||||||
|
self.reportTestStart(test_desc)
|
||||||
|
test_ok = False
|
||||||
|
try:
|
||||||
|
for v in pwm_vals:
|
||||||
|
fan.setPwmValue(v)
|
||||||
|
fan.waitStabilize()
|
||||||
|
test_ok = not fan.isRunning()
|
||||||
|
if test_ok:
|
||||||
|
stop_value = v
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.reportTestResult(test_desc, test_ok)
|
||||||
|
|
||||||
|
start_stop_values.append((start_value, stop_value))
|
||||||
|
|
||||||
|
return start_stop_values
|
||||||
|
|
||||||
|
def reportTestGroupStart(self, desc):
|
||||||
|
print("%s %s" % (desc, "-" * (shutil.get_terminal_size()[0] - len(desc) - 1)))
|
||||||
|
|
||||||
|
def reportTestStart(self, desc):
|
||||||
|
print(desc, end=" ", flush=True)
|
||||||
|
|
||||||
|
def reportTestResult(self, desc, ok):
|
||||||
|
if ok:
|
||||||
|
self.ok_count += 1
|
||||||
|
else:
|
||||||
|
self.ko_count += 1
|
||||||
|
print(("[ %s ]" % ("OK" if ok else "KO")).rjust(shutil.get_terminal_size()[0] - len(desc) - 1))
|
||||||
|
|
||||||
|
|
||||||
|
def test(drive_filepaths, fan_pwm_filepaths, stat_filepaths):
|
||||||
|
fans = [Fan(i, fan_pwm_filepath, 0, 0) for i, fan_pwm_filepath in enumerate(fan_pwm_filepaths, 1)]
|
||||||
|
drives = [Drive(drive_filepath,
|
||||||
|
stat_filepath) for drive_filepath, stat_filepath in zip(drive_filepaths,
|
||||||
|
stat_filepaths)]
|
||||||
|
|
||||||
|
tester = TestHardware(drives, fans)
|
||||||
|
tester.run()
|
||||||
|
|
||||||
|
|
||||||
def main(drive_filepaths, fan_pwm_filepaths, fan_start_values, fan_stop_values, min_fan_speed_prct, min_temp, max_temp,
|
def main(drive_filepaths, fan_pwm_filepaths, fan_start_values, fan_stop_values, min_fan_speed_prct, min_temp, max_temp,
|
||||||
interval_s, spin_down_time_s, stat_filepaths):
|
interval_s, spin_down_time_s, stat_filepaths):
|
||||||
try:
|
try:
|
||||||
@@ -320,19 +477,19 @@ if __name__ == "__main__":
|
|||||||
dest="fan_pwm_filepath",
|
dest="fan_pwm_filepath",
|
||||||
help="PWM filepath(s) to control fan speed (under /sys)")
|
help="PWM filepath(s) to control fan speed (under /sys)")
|
||||||
arg_parser.add_argument("--pwm-start-value",
|
arg_parser.add_argument("--pwm-start-value",
|
||||||
required=True,
|
|
||||||
type=int,
|
type=int,
|
||||||
|
default=None,
|
||||||
nargs="+",
|
nargs="+",
|
||||||
dest="fan_start_value",
|
dest="fan_start_value",
|
||||||
help="""PWM value (0-255), at which the fan starts moving.
|
help="""PWM value (0-255), at which the fan starts moving.
|
||||||
Run pwmconfig to find this value.""")
|
Use the -t parameter, or run pwmconfig to find this value.""")
|
||||||
arg_parser.add_argument("--pwm-stop-value",
|
arg_parser.add_argument("--pwm-stop-value",
|
||||||
required=True,
|
|
||||||
type=int,
|
type=int,
|
||||||
|
default=None,
|
||||||
nargs="+",
|
nargs="+",
|
||||||
dest="fan_stop_value",
|
dest="fan_stop_value",
|
||||||
help="""PWM value (0-255), at which the fan stop moving.
|
help="""PWM value (0-255), at which the fan stop moving.
|
||||||
Run pwmconfig to find this value.
|
Use the -t parameter, or run pwmconfig to find this value.
|
||||||
Often 20-40 lower than start speed.""")
|
Often 20-40 lower than start speed.""")
|
||||||
arg_parser.add_argument("--min-temp",
|
arg_parser.add_argument("--min-temp",
|
||||||
type=int,
|
type=int,
|
||||||
@@ -390,9 +547,16 @@ if __name__ == "__main__":
|
|||||||
default=None,
|
default=None,
|
||||||
dest="pid_filepath",
|
dest="pid_filepath",
|
||||||
help="Filepath for lock file when using deamon mode")
|
help="Filepath for lock file when using deamon mode")
|
||||||
|
arg_parser.add_argument("-t",
|
||||||
|
"--test",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
dest="test_mode",
|
||||||
|
help="Run some tests and exit")
|
||||||
args = arg_parser.parse_args()
|
args = arg_parser.parse_args()
|
||||||
if not (len(args.fan_pwm_filepath) == len(args.fan_start_value) == len(args.fan_stop_value) ==
|
if ((len(args.fan_pwm_filepath) != len(args.stat_filepaths)) or
|
||||||
len(args.stat_filepaths)):
|
((args.fan_start_value is not None) and (len(args.fan_pwm_filepath) != len(args.fan_start_value))) or
|
||||||
|
((args.fan_stop_value is not None) and (len(args.fan_pwm_filepath) != len(args.fan_stop_value)))):
|
||||||
raise ValueError("Invalid parameter count")
|
raise ValueError("Invalid parameter count")
|
||||||
|
|
||||||
# setup logger
|
# setup logger
|
||||||
@@ -407,29 +571,37 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# check if root
|
# check if root
|
||||||
if os.geteuid() != 0:
|
if os.geteuid() != 0:
|
||||||
logging.getLogger().error("You need to run this script as root")
|
logging.getLogger("Startup").error("You need to run this script as root")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
# main
|
if args.test_mode or (args.fan_start_value is None) or (args.fan_stop_value is None):
|
||||||
with contextlib.ExitStack() as deamon_context:
|
if (args.fan_start_value is None) or (args.fan_stop_value is None):
|
||||||
if args.daemonize:
|
logging.getLogger("Startup").warning("Missing --pwm-start-value or --pwm-stop-value argument, running hardware test to find values")
|
||||||
preserved_fds = None
|
test(args.drive_filepaths,
|
||||||
if args.log_filepath is not None:
|
args.fan_pwm_filepath,
|
||||||
log_output = deamon_context.enter_context(open(args.log_filepath, "at+"))
|
args.stat_filepaths)
|
||||||
preserved_fds = [log_output.fileno()]
|
|
||||||
else:
|
else:
|
||||||
log_output = None
|
# main
|
||||||
if args.pid_filepath is not None:
|
with contextlib.ExitStack() as deamon_context:
|
||||||
pidfile = daemon.pidlockfile.PIDLockFile(args.pid_filepath)
|
if args.daemonize:
|
||||||
if pidfile.is_locked():
|
preserved_fds = None
|
||||||
logging.getLogger().error("Daemon already running")
|
if args.log_filepath is not None:
|
||||||
exit(1)
|
log_output = deamon_context.enter_context(open(args.log_filepath, "at+"))
|
||||||
else:
|
preserved_fds = [log_output.fileno()]
|
||||||
pidfile = None
|
else:
|
||||||
deamon_context.enter_context(daemon.DaemonContext(stdout=log_output,
|
log_output = None
|
||||||
stderr=log_output,
|
if args.pid_filepath is not None:
|
||||||
pidfile=pidfile,
|
pidfile = daemon.pidlockfile.PIDLockFile(args.pid_filepath)
|
||||||
files_preserve=preserved_fds))
|
if pidfile.is_locked():
|
||||||
|
logging.getLogger().error("Daemon already running")
|
||||||
|
exit(1)
|
||||||
|
else:
|
||||||
|
pidfile = None
|
||||||
|
deamon_context.enter_context(daemon.DaemonContext(stdout=log_output,
|
||||||
|
stderr=log_output,
|
||||||
|
pidfile=pidfile,
|
||||||
|
files_preserve=preserved_fds))
|
||||||
main(args.drive_filepaths,
|
main(args.drive_filepaths,
|
||||||
args.fan_pwm_filepath,
|
args.fan_pwm_filepath,
|
||||||
args.fan_start_value,
|
args.fan_start_value,
|
||||||
|
|||||||
Reference in New Issue
Block a user