#! /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)