fand/fand.py

152 lines
3.3 KiB
Python
Executable file

#! /usr/bin/env python3
# encoding: utf-8
"""
Fan control service.
Measures temperatures and sets fan PWM according to a configured curve.
See /etc/fand/config.json.example for more.
"""
import dec8
import os
import sys
import time
class Sensor:
def __init__(self, source, weight=0.2):
self.f = open(source, "rb")
self.weight = weight
self.value = None
def measure(self):
self.f.seek(0)
raw = int(self.f.read())
if self.value is None:
self.value = raw
else:
self.value = int(self.weight * raw + (1-self.weight) * self.value)
return self.value
class PWM:
def __init__(self, chip, ch, period):
self.chip_base = "/sys/class/pwm/pwmchip%d/" %(chip)
self.ch_base = "/sys/class/pwm/pwmchip%d/pwm%d/" %(chip, ch)
self.period = period
self.duty_cycle = 0
# export the channel to sysfs if necessary
if not os.path.isdir(self.ch_base):
with open(self.chip_base + "export", "w") as f:
f.write(str(ch))
# disable channel
self.set_enabled(False)
# acquire a duty cycle file descriptor
self.f_dc = open(self.ch_base + "duty_cycle", "w")
# Set 0 duty cycle so period can be freely set even if the channel was used before.
# This may fail: ignore.
try:
self.set_duty_cycle(0)
except OSError:
pass
# set period & polarity
with open(self.ch_base + "period", "w") as f:
f.write(str(period))
with open(self.ch_base + "polarity", "w") as f:
f.write("normal")
# re-enable channel
self.set_enabled(True)
def set_enabled(self, enabled):
with open(self.ch_base + "enable", "r+") as f:
current = f.read().strip()
requested = "1" if enabled else "0"
if current != requested:
f.seek(0)
f.write(requested)
def set_percent(self, pc):
self.set_duty_cycle(self.period * pc/100)
def set_duty_cycle(self, dc):
self.duty_cycle = int(dc)
self.f_dc.seek(0)
self.f_dc.write(str(self.duty_cycle))
self.f_dc.flush()
def linlut(a, seq):
"""
Given a value `a` and a sorted list of tuples `seq`,
finds a pair in `seq` whose first elements are closest
to ... well you know what I mean. Read the code. It's
shorter than this paragraph.
"""
smaller = None
larger = None
for d in seq:
if d[0] <= a:
smaller = d
elif d[0] > a:
larger = d
break
if not smaller:
return seq[0][1]
if not larger:
return seq[-1][1]
ratio = (a - smaller[0]) / (larger[0] - smaller[0])
dy = larger[1] - smaller[1]
return smaller[1] + ratio * dy
def printf(fmt, *args, stream=sys.stderr):
print(fmt % args, file=stream, flush=True)
if __name__ == "__main__":
try:
cfg = dec8.cfg.Config.from_file("/etc/fand/config.json")
except FileNotFoundError:
printf("!!! missing /etc/fand/config.json")
s = Sensor(cfg.sensor.source, cfg.sensor.weight)
p = PWM(cfg.pwm.chip, cfg.pwm.channel, cfg.pwm.period)
if len(sys.argv) == 3 and sys.argv[1] == "--set":
try:
percentage = int(sys.argv[2])
except:
print("--set must be followed by a number")
sys.exit(1)
p.set_percent(percentage)
sys.exit(0)
while True:
temperature = s.measure()
percentage = linlut(temperature, cfg.map)
p.set_percent(percentage)
if cfg.control.verbose:
printf("%.01f °C -> %d %% (%d/%d µs)",
temperature / 1000,
percentage,
p.duty_cycle // 1000,
p.period // 1000
)
time.sleep(cfg.control.interval)