Initial commit
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
@@ -0,0 +1,7 @@
|
||||
import shutil
|
||||
|
||||
|
||||
def check_bin_dependency(bins):
|
||||
for bin in bins:
|
||||
if shutil.which(bin) is None:
|
||||
raise RuntimeError("Binary '%s' could not be found" % (bin))
|
||||
@@ -0,0 +1,30 @@
|
||||
""" Formatter for the logging module, coloring terminal output according to error criticity. """
|
||||
|
||||
import enum
|
||||
import logging
|
||||
import sys
|
||||
|
||||
|
||||
Colors = enum.Enum("Colors", ("RED", "GREEN", "YELLOW", "BLUE"))
|
||||
|
||||
LEVEL_COLOR_MAPPING = {logging.WARNING: Colors.YELLOW,
|
||||
logging.ERROR: Colors.RED,
|
||||
logging.CRITICAL: Colors.RED}
|
||||
LEVEL_BOLD_MAPPING = {logging.WARNING: False,
|
||||
logging.ERROR: False,
|
||||
logging.CRITICAL: True}
|
||||
|
||||
|
||||
class ColoredFormatter(logging.Formatter):
|
||||
|
||||
def format(self, record):
|
||||
message = super().format(record)
|
||||
if sys.stderr.isatty() and not sys.platform.startswith("win32"):
|
||||
try:
|
||||
color_code = LEVEL_COLOR_MAPPING[record.levelno].value
|
||||
bold = LEVEL_BOLD_MAPPING[record.levelno]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
message = "\033[%u;%um%s\033[0m" % (int(bold), 30 + color_code, message)
|
||||
return message
|
||||
Executable
+442
@@ -0,0 +1,442 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
""" Dynamically control fan speed according to hard drive temperature. """
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import enum
|
||||
import logging
|
||||
import operator
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
|
||||
import daemon
|
||||
import daemon.pidlockfile
|
||||
|
||||
import bin_dep
|
||||
import colored_logging
|
||||
|
||||
|
||||
class Drive:
|
||||
|
||||
""" Drive represented by a device file like /dev/sdX. """
|
||||
|
||||
DriveState = enum.Enum("DriveState", ("UNKNOWN", "ACTIVE_IDLE", "STANDBY", "SLEEPING"))
|
||||
|
||||
def __init__(self, device_filepath, stat_filepath):
|
||||
assert(stat.S_ISBLK(os.stat(device_filepath).st_mode))
|
||||
self.device_filepath = device_filepath
|
||||
self.stat_filepath = stat_filepath
|
||||
self.logger = logging.getLogger(str(self))
|
||||
# test if drive supports hdparm -H
|
||||
cmd = ("hdparm", "-H", self.device_filepath)
|
||||
try:
|
||||
subprocess.check_call(cmd,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
except subprocess.CalledProcessError:
|
||||
self.logger.warning("Drive does not allow querying temperature without going out of low power mode.")
|
||||
self.supports_hitachi_temp_query = False
|
||||
else:
|
||||
self.supports_hitachi_temp_query = True
|
||||
|
||||
def __str__(self):
|
||||
""" Return a pretty drive name. """
|
||||
return os.path.basename(self.device_filepath).rsplit("-", 1)[-1]
|
||||
|
||||
def getState(self):
|
||||
""" Get drive power state, as a DriveState enum. """
|
||||
states = {"unknown": __class__.DriveState.UNKNOWN,
|
||||
"active/idle": __class__.DriveState.ACTIVE_IDLE,
|
||||
"standby": __class__.DriveState.STANDBY,
|
||||
"sleeping": __class__.DriveState.SLEEPING}
|
||||
cmd = ("hdparm", "-C", self.device_filepath)
|
||||
output = subprocess.check_output(cmd,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
universal_newlines=True)
|
||||
str_state = output.rsplit(" ", 1)[-1].strip()
|
||||
state = states[str_state]
|
||||
self.logger.debug("Drive state: %s" % (state.name))
|
||||
return state
|
||||
|
||||
def isSleeping(self):
|
||||
""" Return True if drive is in low power state, False otherwise. """
|
||||
return (self.getState() in (Drive.DriveState.STANDBY, Drive.DriveState.SLEEPING))
|
||||
|
||||
def getTemperature(self):
|
||||
""" Get drive temperature in Celcius using either hddtemp or hdparm. """
|
||||
if not self.supports_hitachi_temp_query:
|
||||
cmd = ("hddtemp", "-u", "C", "-n", self.device_filepath)
|
||||
output = subprocess.check_output(cmd,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
universal_newlines=True)
|
||||
temp = int(output.strip())
|
||||
else:
|
||||
cmd = ("hdparm", "-H", self.device_filepath)
|
||||
output = subprocess.check_output(cmd,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
universal_newlines=True)
|
||||
temp = int(re.search("drive temperature \(celsius\) is:\s*([0-9]*)", output).group(1))
|
||||
self.logger.debug("Drive temperature: %u°C" % (temp))
|
||||
return temp
|
||||
|
||||
def spinDown(self):
|
||||
""" Spin down a drive, effectively setting it to DriveState.STANDBY state. """
|
||||
logging.getLogger().info("Spinning down drive %s" % (self))
|
||||
cmd = ("hdparm", "-y", self.device_filepath)
|
||||
subprocess.check_call(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
def getActivityStats(self):
|
||||
""" Return drive stats as in /proc/diskstats, as a tuple of integer. """
|
||||
with open(self.stat_filepath, "rt") as stat_file:
|
||||
stats = stat_file.read()
|
||||
stats = filter(None, map(str.strip, stats.strip().split(" ")))
|
||||
stats = tuple(map(int, stats))
|
||||
return stats
|
||||
|
||||
|
||||
class DriveSpinDownThread(threading.Thread):
|
||||
|
||||
""" Thread responsible for spinning down a drive when it is not active for a certain amount of time. """
|
||||
|
||||
def __init__(self, drive, spin_down_time_s):
|
||||
self.drive = drive
|
||||
self.spin_down_time_s = spin_down_time_s
|
||||
super().__init__(name="%s-%s" % (__class__.__name__, drive))
|
||||
|
||||
def run(self):
|
||||
""" Thread loop. """
|
||||
try:
|
||||
logger = logging.getLogger(self.name)
|
||||
previous_stats = None
|
||||
while True:
|
||||
if self.drive.isSleeping():
|
||||
logger.debug("Drive is already sleeping")
|
||||
time.sleep(60)
|
||||
continue
|
||||
|
||||
if previous_stats is None:
|
||||
# get stats
|
||||
previous_stats = self.drive.getActivityStats()
|
||||
previous_stats_time = time.time()
|
||||
|
||||
# sleep
|
||||
time.sleep(min(self.spin_down_time_s, 60))
|
||||
|
||||
# get stats again
|
||||
stats = self.drive.getActivityStats()
|
||||
now = time.time()
|
||||
if stats != previous_stats:
|
||||
logger.debug("Drive is active")
|
||||
previous_stats = None
|
||||
else:
|
||||
delta = now - previous_stats_time
|
||||
logger.debug("Drive is inactive for %u min" % (int(delta / 60)))
|
||||
if delta > self.spin_down_time_s:
|
||||
self.drive.spinDown()
|
||||
|
||||
except Exception as e:
|
||||
logging.getLogger().error(e)
|
||||
|
||||
|
||||
class Fan:
|
||||
|
||||
""" Represent a fan associated with a PWM file to control its speed. """
|
||||
|
||||
def __init__(self, id, pwm_filepath, start_value, stop_value):
|
||||
assert(0 <= start_value <= 255)
|
||||
assert(0 <= stop_value <= 255)
|
||||
self.id = id
|
||||
self.pwm_filepath = pwm_filepath
|
||||
self.start_value = start_value
|
||||
self.stop_value = stop_value
|
||||
self.startup = False
|
||||
self.logger = logging.getLogger("Fan #%u" % (self.id))
|
||||
|
||||
def getRpm(self):
|
||||
""" Read fan speed in revolutions per minute. """
|
||||
pwm_num = int(re.search("[^0-9]*([0-9]*)$", self.pwm_filepath).group(1))
|
||||
fan_input_filepath = os.path.join(os.path.dirname(self.pwm_filepath), "fan%u_input" % (pwm_num))
|
||||
with open(fan_input_filepath, "rt") as fan_input_file:
|
||||
rpm = int(fan_input_file.read().strip())
|
||||
self.logger.debug("Rotation speed is currently %u rpm" % (rpm))
|
||||
return rpm
|
||||
|
||||
def isRunning(self):
|
||||
""" Return True if fan is moving, False instead. """
|
||||
return (self.getRpm() > 0)
|
||||
|
||||
def setSpeed(self, speed_prct, min_prct):
|
||||
""" Set fan speed to a percentage of its maximum speed. """
|
||||
# preconditions
|
||||
assert(0 <= speed_prct <= 100)
|
||||
assert(0 <= min_prct <= 100)
|
||||
|
||||
self.logger.info("Setting fan speed to %u%%" % (speed_prct))
|
||||
|
||||
# calculate target PWM value
|
||||
if speed_prct == 0:
|
||||
target_value = 0
|
||||
else:
|
||||
target_value = self.stop_value + ((255 - self.stop_value) * speed_prct) // 100
|
||||
if min_prct > 0:
|
||||
min_value = self.stop_value + ((255 - self.stop_value) * min_prct) // 100
|
||||
target_value = max(min_value, target_value)
|
||||
|
||||
if (0 < target_value < self.start_value) and (not self.isRunning()):
|
||||
self.startup = True
|
||||
else:
|
||||
self.startup = False
|
||||
|
||||
# set speed
|
||||
if self.startup:
|
||||
# fan startup boost
|
||||
self.logger.debug("Applying startup boost")
|
||||
self.setPwmValue(self.start_value)
|
||||
else:
|
||||
self.setPwmValue(target_value)
|
||||
|
||||
def setPwmValue(self, value):
|
||||
""" Set fan PWM value. """
|
||||
assert(0 <= value <= 255)
|
||||
assert(not stat.S_ISBLK(os.stat(self.pwm_filepath).st_mode)) # check we are not mistakenly writing to a device file
|
||||
enabled_filepath = "%s_enable" % (self.pwm_filepath)
|
||||
with open(enabled_filepath, "r+t") as enabled_file:
|
||||
enabled_val = int(enabled_file.read().strip())
|
||||
if enabled_val != 1:
|
||||
self.logger.warning("%s was %u, setting it to 1", enabled_filepath, enabled_val)
|
||||
enabled_file.seek(0)
|
||||
enabled_file.write("1")
|
||||
with open(self.pwm_filepath, "wt") as pwm_file:
|
||||
self.logger.debug("Setting PWM value to %u" % (value))
|
||||
pwm_file.write("%u" % (value))
|
||||
|
||||
|
||||
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):
|
||||
try:
|
||||
fans = [Fan(i,
|
||||
fan_pwm_filepath,
|
||||
fan_start_value,
|
||||
fan_stop_value) for i,
|
||||
(fan_pwm_filepath,
|
||||
fan_start_value,
|
||||
fan_stop_value) in enumerate(zip(fan_pwm_filepaths,
|
||||
fan_start_values,
|
||||
fan_stop_values),
|
||||
1)]
|
||||
drives = [Drive(drive_filepath,
|
||||
stat_filepath) for drive_filepath, stat_filepath in zip(drive_filepaths,
|
||||
stat_filepaths)]
|
||||
|
||||
drives_startup_time = time.time()
|
||||
|
||||
# start spin down threads if needed
|
||||
spin_down_threads = []
|
||||
if (spin_down_time_s is not None) and (spin_down_time_s > interval_s):
|
||||
for drive in drives:
|
||||
spin_down_threads.append(DriveSpinDownThread(drive, spin_down_time_s))
|
||||
for thread in spin_down_threads:
|
||||
thread.start()
|
||||
|
||||
while True:
|
||||
now = time.time()
|
||||
|
||||
# calc max drive temperature
|
||||
temps = []
|
||||
awakes = []
|
||||
logger = logging.getLogger("Fan speed control")
|
||||
for drive in drives:
|
||||
awake = not drive.isSleeping()
|
||||
awakes.append(awake)
|
||||
if awake or drive.supports_hitachi_temp_query:
|
||||
temps.append(drive.getTemperature())
|
||||
else:
|
||||
logger.debug("Drive %s is in low power state, unable to query temperature" % (drive))
|
||||
if temps:
|
||||
temp = max(temps)
|
||||
logger.info("Maximum drive temperature: %u°C" % (temp))
|
||||
else:
|
||||
assert(not any(awakes))
|
||||
logger.info("All drives are in low power state")
|
||||
|
||||
if not any(awakes):
|
||||
drives_startup_time = now
|
||||
|
||||
# calc target percentage speed
|
||||
if temps and (temp - min_temp > 0):
|
||||
speed_prct = 100 * (temp - min_temp) // (max_temp - min_temp)
|
||||
speed_prct = int(min(speed_prct, 100))
|
||||
else:
|
||||
speed_prct = 0
|
||||
|
||||
# set speed
|
||||
for fan in fans:
|
||||
fan.setSpeed(speed_prct, min_fan_speed_prct)
|
||||
|
||||
# sleep
|
||||
if any(map(operator.attrgetter("startup"), fans)):
|
||||
# at least one fan is starting up, quickly cancel startup boost
|
||||
current_interval_s = min(10, interval_s)
|
||||
elif (now - drives_startup_time) < (60 * 5):
|
||||
# if all drives were started or waken up less than 5 min ago, dont' sleep too long because they
|
||||
# can heat up quickly
|
||||
current_interval_s = min(20, interval_s)
|
||||
else:
|
||||
current_interval_s = interval_s
|
||||
logger.debug("Sleeping for %u seconds" % (current_interval_s))
|
||||
time.sleep(current_interval_s)
|
||||
|
||||
except Exception as e:
|
||||
logging.getLogger().error(e)
|
||||
|
||||
|
||||
# check deps
|
||||
bin_dep.check_bin_dependency(("hddtemp", "hdparm"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# parse args
|
||||
arg_parser = argparse.ArgumentParser(description=__doc__)
|
||||
arg_parser.add_argument("-d",
|
||||
"--drives",
|
||||
required=True,
|
||||
nargs="+",
|
||||
dest="drive_filepaths",
|
||||
help="Drive(s) to get temperature from (ie. /dev/sdX)")
|
||||
arg_parser.add_argument("-p",
|
||||
"--pwm",
|
||||
required=True,
|
||||
nargs="+",
|
||||
dest="fan_pwm_filepath",
|
||||
help="PWM filepath(s) to control fan speed (under /sys)")
|
||||
arg_parser.add_argument("--pwm-start-value",
|
||||
required=True,
|
||||
type=int,
|
||||
nargs="+",
|
||||
dest="fan_start_value",
|
||||
help="""PWM value (0-255), at which the fan starts moving.
|
||||
Run pwmconfig to find this value.""")
|
||||
arg_parser.add_argument("--pwm-stop-value",
|
||||
required=True,
|
||||
type=int,
|
||||
nargs="+",
|
||||
dest="fan_stop_value",
|
||||
help="""PWM value (0-255), at which the fan stop moving.
|
||||
Run pwmconfig to find this value.
|
||||
Often 20-40 lower than start speed.""")
|
||||
arg_parser.add_argument("--min-temp",
|
||||
type=int,
|
||||
default=30,
|
||||
dest="min_temp",
|
||||
help="Temperature in Celcius at which the fan(s) will be set to minimum speed.")
|
||||
arg_parser.add_argument("--max-temp",
|
||||
type=int,
|
||||
default=50,
|
||||
dest="max_temp",
|
||||
help="Temperature in Celcius at which the fan(s) will be set to maximum speed.")
|
||||
arg_parser.add_argument("--min-fan-speed-prct",
|
||||
type=int,
|
||||
default=20,
|
||||
dest="min_fan_speed_prct",
|
||||
help="""Minimum percentage of full fan speed to set the fan to.
|
||||
Never set to 0 unless you have other fans to cool down your system,
|
||||
or a case specially designed for passive cooling.""")
|
||||
arg_parser.add_argument("-i",
|
||||
"--interval",
|
||||
type=int,
|
||||
default=60,
|
||||
dest="interval_s",
|
||||
help="Interval in seconds to check temperature and adjust fan speed.")
|
||||
arg_parser.add_argument("--stat-files",
|
||||
required=True,
|
||||
nargs="+",
|
||||
dest="stat_filepaths",
|
||||
help="Filepath of drive stats file (ie. /sys/block/sdX/stat)")
|
||||
arg_parser.add_argument("--spin-down-time",
|
||||
type=int,
|
||||
default=None,
|
||||
dest="spin_down_time_s",
|
||||
help="Interval in seconds after which inactive drives will be put to standby state.")
|
||||
arg_parser.add_argument("-v",
|
||||
"--verbosity",
|
||||
action="store",
|
||||
choices=("warning", "normal", "debug"),
|
||||
default="normal",
|
||||
dest="verbosity",
|
||||
help="Level of output to display")
|
||||
arg_parser.add_argument("-b",
|
||||
"--background",
|
||||
action="store_true",
|
||||
dest="daemonize",
|
||||
help="Daemonize process")
|
||||
arg_parser.add_argument("-l",
|
||||
"--log-file",
|
||||
action="store",
|
||||
default=None,
|
||||
dest="log_filepath",
|
||||
help="Filepath for log output when using deamon mode")
|
||||
arg_parser.add_argument("--pid-file",
|
||||
action="store",
|
||||
default=None,
|
||||
dest="pid_filepath",
|
||||
help="Filepath for lock file when using deamon mode")
|
||||
args = arg_parser.parse_args()
|
||||
if not (len(args.fan_pwm_filepath) == len(args.fan_start_value) == len(args.fan_stop_value) ==
|
||||
len(args.stat_filepaths)):
|
||||
raise ValueError("Invalid parameter count")
|
||||
|
||||
# setup logger
|
||||
logging_level = {"warning": logging.WARNING,
|
||||
"normal": logging.INFO,
|
||||
"debug": logging.DEBUG}
|
||||
logging.getLogger().setLevel(logging_level[args.verbosity])
|
||||
logging_formatter = colored_logging.ColoredFormatter(fmt="%(asctime)s %(levelname)s [%(name)s] %(message)s")
|
||||
logging_handler = logging.StreamHandler()
|
||||
logging_handler.setFormatter(logging_formatter)
|
||||
logging.getLogger().addHandler(logging_handler)
|
||||
|
||||
# check if root
|
||||
if os.geteuid() != 0:
|
||||
logging.getLogger().error("You need to run this script as root")
|
||||
exit(1)
|
||||
|
||||
# main
|
||||
with contextlib.ExitStack() as deamon_context:
|
||||
if args.daemonize:
|
||||
preserved_fds = None
|
||||
if args.log_filepath is not None:
|
||||
log_output = deamon_context.enter_context(open(args.log_filepath, "at+"))
|
||||
preserved_fds = [log_output.fileno()]
|
||||
else:
|
||||
log_output = None
|
||||
if args.pid_filepath is not None:
|
||||
pidfile = daemon.pidlockfile.PIDLockFile(args.pid_filepath)
|
||||
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,
|
||||
args.fan_pwm_filepath,
|
||||
args.fan_start_value,
|
||||
args.fan_stop_value,
|
||||
args.min_fan_speed_prct,
|
||||
args.min_temp,
|
||||
args.max_temp,
|
||||
args.interval_s,
|
||||
args.spin_down_time_s,
|
||||
args.stat_filepaths)
|
||||
@@ -0,0 +1 @@
|
||||
-e git+https://github.com/Yuego/python-daemon@1.5.5#egg=python-daemon-py3k
|
||||
Reference in New Issue
Block a user