fand¶
Simple daemon to control fan speed.
Documentation is available at https://fand.readthedocs.io/. The installation chapter provides install instructions and compatibility informations.
About¶
The main executable of this program is the fand-server
daemon.
There are 3 main modules: server, clientrpi and fanctl.
They can be accessed through their respective entry points:
fand-server
, fand-clientrpi
and fanctl
.
They can also be accessed with fand <module-name>
.
A server monitor the hardware and clients connect to it to get data (e.g. fan speed or override a fan speed).
$ fanctl get-rpm shelf1
1500
$ fanctl get-pwm shelf1
50
$ fanctl set-pwm-override shelf1 100
ok
$ fanctl get-pwm shelf1
100
$ fanctl get-rpm shelf1
3000
Server¶
The server module provide a daemon which monitor devices temperatures and find a corresponding fan speed. It listens for connections from clients, and answers to requests.
Fan clients¶
A client is assigned a shelf and will regularly request the server for the fan speed (percentage). It will then ajust the fan to use this speed.
Clients also send the actual fan speed in RPM to the server. This will allow other client to have access to the data from the server.
Table of contents¶
Server¶
The server is a daemon monitoring devices temperatures.
Devices are separated in shelves. Each shelf contains a set of devices. Each device has a type (HDD, SSD, CPU).
Fan speed is determined from the temperature of the device which is in most need of cooling.
For each type of device, an effective temperature is determined from the maximum temperature of all the devices of this type. With this temperature, an effective fan speed is determined. We then have an effective fan speed for each device type, the highest fan speed is then defined as the speed for the entire shelf.
Examples¶
Start the server:
# fand server
Start and listen on 0.0.0.0:1234
:
# fand server -a 0.0.0.0 -P 1234
Start and show very verbose logging:
# fand server -vvv
Configuration file¶
Default configuration file is read from either /etc/fand.ini
, the
FAND_CONFIG
environment variable, or ./fand.ini
.
There is also a -c
parameter to specify the config file path.
The configuration is in the ini
format.
It must have a [DEFAULT]
section wich contain a shelves
key listing
shelves names to use.
This section also contains default configuration for fan speed.
For each shelf, a section with its name has to be defined.
It will contain a devices
key listing devices assigned to this shelf.
It can also override fan speed defined in [DEFAULT]
.
Example configuration file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | # Example configuration file for fand # DEFAULT section, mandatory [DEFAULT] # List shelves, comma separated, mandatory # Each shelf must have a section with its name shelves = shelf1, shelf2 # Default hdd_temps configuration # Dictionnary: `temperature in deg C: speed in percentage` # Example here: if the drive is at 37 deg C, corresponding speed is 30% # if the drive is at 30 deg C, corresponding speed is 25% # if the drive is < 37 deg C, corresponding speed is 25% hdd_temps = 0: 25, 37: 30, 38: 40, 39: 50, 40: 75, 41: 100 # Default ssd_temps configuration # Same format as hdd_temps, but the values are for SSD rather than HDD ssd_temps = 0: 25, 60: 40, 62.5: 50, 65: 70, 67.5: 90, 70: 100 # Default cpu_temps configuration # Same format as hdd_temps, but the values are for the CPU rather than HDD cpu_temps = 0: 25, 75: 40, 80: 60, 85: 80, 90: 100 # Configuration for shelf shelf1 [shelf1] # List of devices in shelf shelf1, mandatory, newline separated # Each line is `serial; position` # serial: serial number of the device # position: position information about the drive, used to help locate it devices = AD7E4EE5B03693D6; drive 1,1 F63414EF35A424FB; drive 1,2 C198DB33426BE180; system drive # Configuration for shelf shelf2 [shelf2] # List of devices devices = 062110D2532377B5; small drive 2,1 FFBEB97C6ED5953B; system drive # Override hdd_temps for this shelf # This configuration will be used for this shelf only hdd_temps = 0: 40, 37: 50, 38: 60, 39: 75, 40: 85, 41: 100 |
Python API¶
-
fand.server.
REQUEST_HANDLERS
¶ Dictionnary assigning a Request to a function
Device¶
-
class
fand.server.
Device
(serial: str, position: str)[source]¶ Class handling devices to get temperature from
Parameters: - serial – Device serial number
- position – Device positionning information
-
position
= None¶ Device positionning information
-
serial
= None¶ Device serial number
-
temperature
¶ Current drive temperature
-
type
¶ DeviceType
Shelf¶
-
class
fand.server.
Shelf
(identifier: str, devices: Iterable[fand.server.Device], sleep_time: float = 60, hdd_temps: Optional[Dict[float, float]] = None, ssd_temps: Optional[Dict[float, float]] = None, cpu_temps: Optional[Dict[float, float]] = None)[source]¶ Class handling shelf data
Parameters: - identifier – Shelf identifier (name)
- devices – Iterable of Device objects
- sleep_time – How many seconds to wait between each shelf update
- hdd_temps – Dictionnary in the format
temperature: speed
, temperature in Celcius, speed in percent, must have a 0 deg key - ssd_temps – Dictionnary in the format
temperature: speed
, temperature in Celcius, speed in percent, must have a 0 deg key - cpu_temps – Dictionnary in the format
temperature: speed
, temperature in Celcius, speed in percent, must have a 0 deg key
Raises: ShelfTemperatureBadValue – One of the temps dictionnary is invalid
-
pwm
¶ Get shelf PWM value
Reading get the effective PWM value. Changing override the PWM value.
Raises: ShelfPwmBadValue – Invalid value
-
pwm_expire
¶ Set the PWM override expiration date, defaults to local timezone
Raises: ShelfPwmExpireBadValue – Invalid value
-
rpm
¶ Shelf fan speed RPM
Raises: ShelfRpmBadValue – Invalid value
add_shelf¶
listen_client¶
read_config¶
-
fand.server.
read_config
(config_file: Optional[str] = None) → Iterable[fand.server.Shelf][source]¶ Read configuration from a file, returns an iterable of shelves
Parameters: config_file – Configuration file to use, defaults to the FAND_CONFIG
environment variable or./fand.ini
or/etc/fand.ini
Raises: ServerNoConfigError – Configuration not found
shelf_thread¶
-
fand.server.
shelf_thread
(shelf: fand.server.Shelf) → None[source]¶ Monitor a shelf
Stops when
fand.util.terminating()
is True or when an unexpected exception occur.Parameters: shelf – Shelf to monitor
daemon¶
-
fand.server.
daemon
(config_file: Optional[str] = None, address: str = 'build-17980832-project-622900-fand', port: int = 9999) → None[source]¶ Main function
Parameters: - config_file – Configuration file to use, defaults to
the
FAND_CONFIG
environment variable or./fand.ini
or/etc/fand.ini
- address – Address of the interface to listen on, defaults to hostname
- port – Port to listen on
Raises: ListeningError – Error while listening for new connections
- config_file – Configuration file to use, defaults to
the
Client: Raspberry Pi¶
The Raspberry Pi client control the fan speed by sending a PWM signal through the GPIO pins.
Two pins are used:
- The PWM pin used to output the PWM signal regulating the fan speed. It should be connected to the PWM input of the fans.
- The RPM pin used to read the actual fan speed in RPM. It should be connected to the tachometer output of the fans.
PWM backend¶
By default, gpiozero
will use whatever supported library is
installed.
To manually set which backend to use, you can use the
GPIOZERO_PIN_FACTORY
environment variable.
See the gpiozero.pins
documentation for more information.
Examples¶
Start the client:
# fand clientrpi
Start and use GPIO pin 17 for PWM, and pin 18 for tacho:
# fand server -W 17 -r 18
Start with verbose output and connect to server at server-host:1234:
# fand server -v -a server-host -P 1234
Python API¶
-
fand.clientrpi.
SLEEP_TIME
= 60¶ How much time to wait between updates
GpioRpm¶
-
class
fand.clientrpi.
GpioRpm
(pin: int, managed: bool = True)[source]¶ Class to handle RPM tachometer input from a fan
Parameters: - pin – GPIO pin number to use
- managed – set to true to have the GPIO device automatically closed
when
fand.util.terminate()
is called
Raises: GpioError – Received a
gpiozero.GPIOZeroError
-
close
() → None[source]¶ Close the GPIO device
Raises: GpioError – Received a gpiozero.GPIOZeroError
-
rpm
= None¶ RPM value
GpioPwm¶
-
class
fand.clientrpi.
GpioPwm
(pin: int, managed: bool = True)[source]¶ Class to handle PWM output for a fan
Parameters: - pin – GPIO pin number to use
- managed – set to true to have the GPIO device automatically closed
when
fand.util.terminate()
is called
Raises: GpioError – Received a
gpiozero.GPIOZeroError
-
close
() → None[source]¶ Close the GPIO device
Raises: GpioError – Received a gpiozero.GPIOZeroError
-
pwm
¶ PWM output value, backend is
gpiozero.PWMLED.value
Raises: GpioError – Received a gpiozero.GPIOZeroError
add_gpio_device¶
-
fand.clientrpi.
add_gpio_device
(device: Union[GpioRpm, GpioPwm]) → None[source]¶ Add a GPIO device to the set of managed GPIO devices
Parameters: device – GPIO device to add Raises: TerminatingError – Trying to add a socket but fand.util.terminating()
is True
daemon¶
-
fand.clientrpi.
daemon
(gpio_pwm: fand.clientrpi.GpioPwm, gpio_rpm: fand.clientrpi.GpioRpm, shelf_name: str = 'build-17980832-project-622900-fand', address: str = 'build-17980832-project-622900-fand', port: int = 9999) → None[source]¶ Main function of this module
Parameters: - gpio_pwm – GPIO device to use for PWM output
- gpio_rpm – GPIO device to use for RPM input
- shelf_name – Name of this shelf, used to communicate with the server
- address – Server address or hostname
- port – Port number to connect to
Command-line interface¶
fanctl
is a CLI allowing to interract with the server.
It is basically a fand client, but does not act as a fan controller.
The user can get the assigned fan speed in percentage, the real fan speed in rpm.
The user can override the assigned fan speed. The override can also be set to expire in a given amount of time, or expire at a given date and time.
Examples¶
Ping the server at 192.168.1.10:1234:
$ fanctl -a 192.168.1.10 -P 1234 ping
Get the assigned fan speed for shelf ‘shelf1’:
$ fanctl get-pwm shelf1
Override fan speed of ‘myshelf’ to 100%:
$ fanctl set-pwm-override myshelf 100
Remove override in 1 hour and 30 minutes:
$ fanctl set-pwm-expire-in myshelf 1h30m
Remove override now:
$ fanctl set-pwm-override myshelf none
Python API¶
-
fand.fanctl.
DATETIME_DATE_FORMATS
¶ List of accepted string formats for
datetime.datetime.strptime()
-
fand.fanctl.
DATETIME_DURATION_FORMATS
¶ List of accepted regex formats for
datetime.timedelta
-
fand.fanctl.
ACTION_DICT
¶ Dictionnary associating action strings to their corresponding functions
send¶
-
fand.fanctl.
send
(action: str, *args, address: str = 'build-17980832-project-622900-fand', port: int = 9999) → None[source]¶ Main function of this module
Parameters: - action – Action to call
- args – Arguments to send to the action
- address – Server address
- port – Server port
Raises: FanctlActionBadValue – Invalid action name or arguments
Communication module¶
The communication module handles the low level communication between the server and the clients.
It provides functions to send and receive a request with arguments to a given socket.
It can start a connection with the server and close a socket. It also keep track of sockets to close them automatically at the end of the program.
Examples¶
from fand.communication import *
s = connect('myserver.example.com', 9999)
send(s, Request.GET_PWM, 'myshelf1')
req, args = recv(s)
if req == Request.SET_PWM:
print("The fan speed of", args[0], "is", args[1])
else:
print("The server did not answer the expected request")
Python API¶
Request¶
-
class
fand.communication.
Request
[source]¶ Bases:
enum.Enum
Enumeration of known requests
-
DISCONNECT
= 'disconnect'¶ Notification of disconnection
-
GET_PWM
= 'get_pwm'¶ Request for a
Request.SET_PWM
to get current PWM
-
GET_RPM
= 'get_rpm'¶ Request for a
Request.SET_RPM
to get current RPM
-
PING
= 'ping'¶ Request for a
Request.ACK
-
SET_PWM
= 'set_pwm'¶ Give the current PWM
-
SET_PWM_EXPIRE
= 'set_pwm_expire'¶ Set the expiration date of the PWM override
-
SET_PWM_OVERRIDE
= 'set_pwm_override'¶ Override the PWM value
-
SET_RPM
= 'set_rpm'¶ Give the current RPM
-
add_socket¶
-
fand.communication.
add_socket
(sock: socket.socket) → None[source]¶ Add sock to the set of managed sockets
It can be removed with
reset_connection()
and will automatically be whenfand.util.terminate()
is called.Parameters: sock – Socket to add Raises: TerminatingError – Trying to add a socket but fand.util.terminating()
is True
is_socket_open¶
-
fand.communication.
is_socket_open
(sock: socket.socket) → bool[source]¶ Returns True if sock is currently managed by this module
This will be False after a socket has been closed with
reset_connection()
.Parameters: sock – Socket to test
send¶
-
fand.communication.
send
(sock: socket.socket, request: fand.communication.Request, *args) → None[source]¶ Send a request to a remote socket
Parameters: - sock – Socket to send the request to
- request – Request to send
- args – Request arguments
Raises: - UnpicklableError – Given data cannot be pickled by
pickle
- FandTimeoutError – Connection timed out
- SendReceiveError – Error while sending the data
recv¶
-
fand.communication.
recv
(sock: socket.socket) → Tuple[fand.communication.Request, Tuple][source]¶ Receive a request from a remote socket, returns (request, args)
Parameters: sock – Socket to receive the request and its arguments from
Raises: - FandTimeoutError – Connection timed out
- SendReceiveError – Error while receiving the data
- FandConnectionResetError – No data received or
Request.DISCONNECT
received - CorruptedDataError – Invalid data received
connect¶
-
fand.communication.
connect
(address: str, port: int) → socket.socket[source]¶ Connect to server and returns socket
Parameters: - address – Server address
- port – Server port
Raises: - FandTimeoutError – Connection timed out
- ConnectionFailedError – Failed to connect to remote socket
reset_connection¶
Utilities module¶
The util
module provides some functions used by most fand modules.
It provides a terminate()
function to make the daemon and its threads
terminate cleanly.
It provide a when_terminate()
function decorator to add
functions to call when terminate()
is called, allowing custom cleanup
from modules.
It also has a default signal handler, and a default argument parser.
Python API¶
terminate¶
-
fand.util.
terminate
(error: Optional[str] = None) → None[source]¶ Function terminating the program
Sets the terminate flag (see
terminating()
), and does some cleanup (seewhen_terminate()
)Parameters: error – Error message to print
sys_exit¶
-
fand.util.
sys_exit
() → NoReturn[source]¶ Exit the program with the error from
terminate()
if any
terminating¶
when_terminate¶
sleep¶
default_signal_handler¶
Exceptions¶
The exception module provides fand-specific exceptions.
All exceptions raised by fand descend from FandError
.
Certain errors have multiple parents.
For instance, ShelfNotFoundError
is a FandError
,
but also a ValueError
.
Python API¶
Common fand errors¶
-
exception
fand.exceptions.
TerminatingError
[source]¶ Bases:
fand.exceptions.FandError
Daemon is terminating
Server and clients specific errors¶
-
exception
fand.exceptions.
ServerNoConfigError
[source]¶ Bases:
fand.exceptions.FandError
,FileNotFoundError
No configuration file found
-
exception
fand.exceptions.
GpioError
[source]¶ Bases:
fand.exceptions.FandError
Any GPIO related errors
-
exception
fand.exceptions.
FanctlActionBadValue
[source]¶ Bases:
fand.exceptions.FandError
,ValueError
No action found with this name and parameters
Installation¶
Installation¶
Server¶
Install smartmontools on your system with your prefered package manager.
Install fand with:
$ pip install fand[server]
Raspberry Pi client¶
Install fand with one of the following commands:
Install with RPi.GPIO:
$ pip install fand[clientrpi-rpi-gpio]Install with pigpio:
$ pip install fand[clientrpi-pigpio]Install with RPIO:
$ pip install fand[clientrpi-rpio]
Custom installation¶
You can cumulate extra dependencies:
$ pip install fand[server,clientrpi-pigpio]
Documentation¶
To build the documentation, you can install fand with:
$ pip install fand[doc]
Download the fand source code:
$ pip download --no-deps --no-binary fand fand
$ tar -xf <filename>
$ cd <directory>
And build the documentation with:
$ cd doc
$ make html
The documentation will be built in the build
directory.
Testing¶
To run CI or QA tests, you can install fand with:
$ pip install fand[test,qa]
You may want to also install server
and clientrpi-base
dependencies
to test the corresponding modules.
Run the tests with:
$ tox
Python version support¶
Operating system support¶
Server¶
- Linux
- FreeBSD
- Windows: untested, missing support for CPU temperature monitoring (
psutil.sensors_temperatures()
does not supports Windows)
Raspberry Pi client¶
- Linux
- Windows: untested
- FreeBSD: unsupported, missing support for any of the gpiozero’s backend for PWM
Other modules¶
- Any OS with Python
License¶
fand is licensed under the MIT license.
Copyright (c) 2020 Louis Leseur
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.