def test_main_smoke(temp_path): pwm_path = temp_path / "pwm2" pwm_path.write_text("") fan_input_path = temp_path / "fan2_input" fan_input_path.write_text("") with ExitStack() as stack: mocked_fantest = stack.enter_context(patch.object(fantest, "run_fantest")) runner = CliRunner() result = runner.invoke( main, [ "--fan-type", "linux", "--linux-fan-pwm", # "/sys/class/hwmon/hwmon0/device/pwm2", str(pwm_path), # click verifies that this file exists "--linux-fan-input", # "/sys/class/hwmon/hwmon0/device/fan2_input", str(fan_input_path), # click verifies that this file exists "--output-format", "human", "--direction", "increase", "--pwm-step-size", "accurate", ], ) print(result.output) assert result.exit_code == 0 assert mocked_fantest.call_count == 1 args, kwargs = mocked_fantest.call_args assert not args assert kwargs.keys() == {"fan", "pwm_step_size", "output"} assert kwargs["fan"] == ReadWriteFan( fan_speed=LinuxFanSpeed(FanInputDevice(str(fan_input_path))), pwm_read=LinuxFanPWMRead(PWMDevice(str(pwm_path))), pwm_write=LinuxFanPWMWrite(PWMDevice(str(pwm_path))), ) assert kwargs["pwm_step_size"] == 5 assert isinstance(kwargs["output"], HumanMeasurementsOutput)
def pwm_write(pwm_path): pwm_write = LinuxFanPWMWrite(pwm=PWMDevice(str(pwm_path))) # We write to the pwm_enable file values without newlines, # but when they're read back, they might contain newlines. # This hack below is to simulate just that: the written values should # contain newlines. original_pwm_enable = pwm_write._pwm_enable pwm_enable = MagicMock(wraps=original_pwm_enable) pwm_enable.write_text = lambda text: original_pwm_enable.write_text(text + "\n") pwm_write._pwm_enable = pwm_enable return pwm_write
def pwmfan(pwm_path, fan_input_path): fan = LinuxPWMFan( pwm=PWMDevice(str(pwm_path)), fan_input=FanInputDevice(str(fan_input_path)) ) # We write to the pwm_enable file values without newlines, # but when they're read back, they might contain newlines. # This hack below is to simulate just that: the written values should # contain newlines. original_pwm_enable = fan._pwm_enable pwm_enable = MagicMock(wraps=original_pwm_enable) pwm_enable.write_text = lambda text: original_pwm_enable.write_text(text + "\n") fan._pwm_enable = pwm_enable return fan
def pwm_read(pwm_path): return LinuxFanPWMRead(pwm=PWMDevice(str(pwm_path)))
def fantest(*, fan_type: str, linux_fan_pwm: Optional[str], linux_fan_input: Optional[str], arduino_serial_url: Optional[str], arduino_baudrate: int, arduino_pwm_pin: Optional[int], arduino_tacho_pin: Optional[int], output_format: str, direction: str, pwm_step_size: str) -> None: """The PWM fan testing program. This program tests how changing the PWM value of a fan affects its speed. In the beginning the fan would be stopped (by setting it to a minimum PWM value), and then the PWM value would be increased in small steps, while also measuring the speed as reported by the fan. This data would help you to find the effective range of values for the `pwm_line_start` and `pwm_line_end` settings where the correlation between PWM and fan speed is close to linear. Usually its `pwm_line_start = 100` and `pwm_line_end = 240`, but it is individual for each fan. The allowed range for a PWM value is from 0 to 255. Note that the fan would be stopped for some time during the test. If you'll feel nervous, press Ctrl+C to stop the test and return the fan to full speed. Before starting the test ensure that no fan control software is currently controlling the fan you're going to test. """ try: if fan_type == "linux": if not linux_fan_pwm: linux_fan_pwm = click.prompt( "\n%s\nPWM file" % HELP_LINUX_PWM_FILE, type=click.Path(exists=True, dir_okay=False), ) if not linux_fan_input: linux_fan_input = click.prompt( "\n%s\nFan input file" % HELP_LINUX_FAN_INPUT_FILE, type=click.Path(exists=True, dir_okay=False), ) assert linux_fan_pwm is not None assert linux_fan_input is not None fan = LinuxPWMFan( pwm=PWMDevice(linux_fan_pwm), fan_input=FanInputDevice(linux_fan_input)) # type: BasePWMFan elif fan_type == "arduino": if not arduino_serial_url: arduino_serial_url = click.prompt("\n%s\nArduino Serial url" % HELP_ARDUINO_SERIAL_URL, type=str) # typeshed currently specifies `Optional[str]` for `default`, # see https://github.com/python/typeshed/blob/5acc22d82aa01005ea47ef64f31cad7e16e78450/third_party/2and3/click/termui.pyi#L34 # noqa # however the click docs say that `default` can be of any type, # see https://click.palletsprojects.com/en/7.x/prompts/#input-prompts # Hence the `type: ignore`. arduino_baudrate = click.prompt( # type: ignore "\n%s\nBaudrate" % HELP_ARDUINO_BAUDRATE, type=int, default=str(arduino_baudrate), show_default=True, ) if not arduino_pwm_pin and arduino_pwm_pin != 0: arduino_pwm_pin = click.prompt("\n%s\nArduino PWM pin" % HELP_ARDUINO_PWM_PIN, type=int) if not arduino_tacho_pin and arduino_tacho_pin != 0: arduino_tacho_pin = click.prompt( "\n%s\nArduino Tachometer pin" % HELP_ARDUINO_TACHO_PIN, type=int) assert arduino_serial_url is not None arduino_connection = ArduinoConnection( name=ArduinoName("_fantest"), serial_url=arduino_serial_url, baudrate=arduino_baudrate, ) assert arduino_pwm_pin is not None assert arduino_tacho_pin is not None fan = ArduinoPWMFan( arduino_connection, pwm_pin=ArduinoPin(arduino_pwm_pin), tacho_pin=ArduinoPin(arduino_tacho_pin), ) else: raise AssertionError( "unreachable if the `fan_type`'s allowed `values` are in sync") output = { "human": HumanMeasurementsOutput(), "csv": CSVMeasurementsOutput() }[output_format] pwm_step_size_value = { "accurate": PWMValue(5), "fast": PWMValue(25) }[pwm_step_size] if direction == "decrease": pwm_step_size_value = PWMValue(pwm_step_size_value * -1 # a bad PWM value, to be honest ) except KeyboardInterrupt: click.echo("") sys.exit(EXIT_CODE_CTRL_C) try: run_fantest(fan=fan, pwm_step_size=pwm_step_size_value, output=output) except KeyboardInterrupt: click.echo("Fan has been returned to full speed") sys.exit(EXIT_CODE_CTRL_C)
def _parse_fans( config: configparser.ConfigParser, arduino_connections: Mapping[ArduinoName, ArduinoConnection], ) -> Mapping[FanName, PWMFanNorm]: fans = {} # type: Dict[FanName, PWMFanNorm] for section_name in config.sections(): section_name_parts = section_name.split(":", 1) if section_name_parts[0].strip().lower() != "fan": continue fan_name = FanName(section_name_parts[1].strip()) fan = config[section_name] keys = set(fan.keys()) fan_type = fan.get("type", fallback=DEFAULT_FAN_TYPE) keys.discard("type") if fan_type == "linux": pwm = PWMDevice(fan["pwm"]) fan_input = FanInputDevice(fan["fan_input"]) keys.discard("pwm") keys.discard("fan_input") pwmfan = LinuxPWMFan(pwm=pwm, fan_input=fan_input) # type: BasePWMFan elif fan_type == "arduino": arduino_name = ArduinoName(fan["arduino_name"]) keys.discard("arduino_name") pwm_pin = ArduinoPin(fan.getint("pwm_pin")) keys.discard("pwm_pin") tacho_pin = ArduinoPin(fan.getint("tacho_pin")) keys.discard("tacho_pin") if arduino_name not in arduino_connections: raise ValueError("[arduino:%s] section is missing" % arduino_name) pwmfan = ArduinoPWMFan(arduino_connections[arduino_name], pwm_pin=pwm_pin, tacho_pin=tacho_pin) else: raise ValueError("Unsupported FAN type %s. Supported ones are " "`linux` and `arduino`." % fan_type) never_stop = fan.getboolean("never_stop", fallback=DEFAULT_NEVER_STOP) keys.discard("never_stop") pwm_line_start = PWMValue( fan.getint("pwm_line_start", fallback=DEFAULT_PWM_LINE_START)) keys.discard("pwm_line_start") pwm_line_end = PWMValue( fan.getint("pwm_line_end", fallback=DEFAULT_PWM_LINE_END)) keys.discard("pwm_line_end") for pwm_value in (pwm_line_start, pwm_line_end): if not (pwmfan.min_pwm <= pwm_value <= pwmfan.max_pwm): raise RuntimeError( "Incorrect PWM value '%s' for fan '%s': it must be within [%s;%s]" % (pwm_value, fan_name, pwmfan.min_pwm, pwmfan.max_pwm)) if pwm_line_start >= pwm_line_end: raise RuntimeError( "`pwm_line_start` PWM value must be less than `pwm_line_end` for fan '%s'" % (fan_name, )) if keys: raise RuntimeError("Unknown options in the [%s] section: %s" % (section_name, keys)) if fan_name in fans: raise RuntimeError("Duplicate fan section declaration for '%s'" % fan_name) fans[fan_name] = PWMFanNorm( pwmfan, pwm_line_start=pwm_line_start, pwm_line_end=pwm_line_end, never_stop=never_stop, ) if not fans: raise RuntimeError( "No fans found in the config, at least 1 must be specified") return fans
def test_pkg_conf(pkg_conf: Path): daemon_cli_config = DaemonCLIConfig(pidfile=None, logfile=None, exporter_listen_host=None) parsed = parse_config(pkg_conf, daemon_cli_config) assert parsed == ParsedConfig( daemon=DaemonConfig( pidfile="/run/afancontrol.pid", logfile="/var/log/afancontrol.log", interval=5, exporter_listen_host=None, ), report_cmd= ('printf "Subject: %s\nTo: %s\n\n%b" ' '"afancontrol daemon report: %REASON%" root "%MESSAGE%" | sendmail -t' ), triggers=TriggerConfig( global_commands=Actions( panic=AlertCommands(enter_cmd=None, leave_cmd=None), threshold=AlertCommands(enter_cmd=None, leave_cmd=None), ), temp_commands={ TempName("mobo"): Actions( panic=AlertCommands(enter_cmd=None, leave_cmd=None), threshold=AlertCommands(enter_cmd=None, leave_cmd=None), ) }, ), fans={ FanName("hdd"): PWMFanNorm( LinuxPWMFan( PWMDevice("/sys/class/hwmon/hwmon0/device/pwm2"), FanInputDevice( "/sys/class/hwmon/hwmon0/device/fan2_input"), ), pwm_line_start=PWMValue(100), pwm_line_end=PWMValue(240), never_stop=False, ) }, temps={ TempName("mobo"): FileTemp( "/sys/class/hwmon/hwmon0/device/temp1_input", min=TempCelsius(30.0), max=TempCelsius(40.0), panic=None, threshold=None, ) }, mappings={ MappingName("1"): FansTempsRelation( temps=[TempName("mobo")], fans=[FanSpeedModifier(fan=FanName("hdd"), modifier=0.6)], ) }, )
def test_minimal_config() -> None: daemon_cli_config = DaemonCLIConfig(pidfile=None, logfile=None, exporter_listen_host=None) config = """ [daemon] [actions] [temp:mobo] type = file path = /sys/class/hwmon/hwmon0/device/temp1_input [fan: case] pwm = /sys/class/hwmon/hwmon0/device/pwm2 fan_input = /sys/class/hwmon/hwmon0/device/fan2_input [mapping:1] fans = case*0.6, temps = mobo """ parsed = parse_config(path_from_str(config), daemon_cli_config) assert parsed == ParsedConfig( daemon=DaemonConfig( pidfile="/run/afancontrol.pid", logfile=None, exporter_listen_host=None, interval=5, ), report_cmd= ('printf "Subject: %s\nTo: %s\n\n%b" ' '"afancontrol daemon report: %REASON%" root "%MESSAGE%" | sendmail -t' ), triggers=TriggerConfig( global_commands=Actions( panic=AlertCommands(enter_cmd=None, leave_cmd=None), threshold=AlertCommands(enter_cmd=None, leave_cmd=None), ), temp_commands={ TempName("mobo"): Actions( panic=AlertCommands(enter_cmd=None, leave_cmd=None), threshold=AlertCommands(enter_cmd=None, leave_cmd=None), ) }, ), fans={ FanName("case"): PWMFanNorm( LinuxPWMFan( PWMDevice("/sys/class/hwmon/hwmon0/device/pwm2"), FanInputDevice( "/sys/class/hwmon/hwmon0/device/fan2_input"), ), pwm_line_start=PWMValue(100), pwm_line_end=PWMValue(240), never_stop=True, ) }, temps={ TempName("mobo"): FileTemp( "/sys/class/hwmon/hwmon0/device/temp1_input", min=None, max=None, panic=None, threshold=None, ) }, mappings={ MappingName("1"): FansTempsRelation( temps=[TempName("mobo")], fans=[FanSpeedModifier(fan=FanName("case"), modifier=0.6)], ) }, )
def test_example_conf(example_conf: Path): daemon_cli_config = DaemonCLIConfig(pidfile=None, logfile=None, exporter_listen_host=None) parsed = parse_config(example_conf, daemon_cli_config) assert parsed == ParsedConfig( daemon=DaemonConfig( pidfile="/run/afancontrol.pid", logfile="/var/log/afancontrol.log", exporter_listen_host="127.0.0.1:8083", interval=5, ), report_cmd= ('printf "Subject: %s\nTo: %s\n\n%b" ' '"afancontrol daemon report: %REASON%" root "%MESSAGE%" | sendmail -t' ), triggers=TriggerConfig( global_commands=Actions( panic=AlertCommands(enter_cmd=None, leave_cmd=None), threshold=AlertCommands(enter_cmd=None, leave_cmd=None), ), temp_commands={ TempName("hdds"): Actions( panic=AlertCommands(enter_cmd=None, leave_cmd=None), threshold=AlertCommands(enter_cmd=None, leave_cmd=None), ), TempName("mobo"): Actions( panic=AlertCommands(enter_cmd=None, leave_cmd=None), threshold=AlertCommands(enter_cmd=None, leave_cmd=None), ), }, ), fans={ FanName("cpu"): PWMFanNorm( LinuxPWMFan( PWMDevice("/sys/class/hwmon/hwmon0/device/pwm1"), FanInputDevice( "/sys/class/hwmon/hwmon0/device/fan1_input"), ), pwm_line_start=PWMValue(100), pwm_line_end=PWMValue(240), never_stop=True, ), FanName("hdd"): PWMFanNorm( LinuxPWMFan( PWMDevice("/sys/class/hwmon/hwmon0/device/pwm2"), FanInputDevice( "/sys/class/hwmon/hwmon0/device/fan2_input"), ), pwm_line_start=PWMValue(100), pwm_line_end=PWMValue(240), never_stop=False, ), FanName("my_arduino_fan"): PWMFanNorm( ArduinoPWMFan( ArduinoConnection( ArduinoName("mymicro"), "/dev/ttyACM0", # linux # "/dev/cu.usbmodem14201", # macos baudrate=115200, status_ttl=5, ), pwm_pin=ArduinoPin(9), tacho_pin=ArduinoPin(3), ), pwm_line_start=PWMValue(100), pwm_line_end=PWMValue(240), never_stop=True, ), }, temps={ TempName("hdds"): HDDTemp( "/dev/sd?", min=TempCelsius(35.0), max=TempCelsius(48.0), panic=TempCelsius(55.0), threshold=None, hddtemp_bin="hddtemp", ), TempName("mobo"): FileTemp( "/sys/class/hwmon/hwmon0/device/temp1_input", min=TempCelsius(30.0), max=TempCelsius(40.0), panic=None, threshold=None, ), }, mappings={ MappingName("1"): FansTempsRelation( temps=[TempName("mobo"), TempName("hdds")], fans=[ FanSpeedModifier(fan=FanName("cpu"), modifier=1.0), FanSpeedModifier(fan=FanName("hdd"), modifier=0.6), FanSpeedModifier(fan=FanName("my_arduino_fan"), modifier=0.222), ], ), MappingName("2"): FansTempsRelation( temps=[TempName("hdds")], fans=[FanSpeedModifier(fan=FanName("hdd"), modifier=1.0)], ), }, )
def test_readonly_config() -> None: daemon_cli_config = DaemonCLIConfig( pidfile=None, logfile=None, exporter_listen_host=None ) config = """ [daemon] [actions] [temp:mobo] type = file path = /sys/class/hwmon/hwmon0/device/temp1_input [readonly_fan: cpu] pwm = /sys/class/hwmon/hwmon0/device/pwm1 fan_input = /sys/class/hwmon/hwmon0/device/fan1_input """ parsed = parse_config(path_from_str(config), daemon_cli_config) assert parsed == ParsedConfig( arduino_connections={}, daemon=DaemonConfig( pidfile="/run/afancontrol.pid", logfile=None, exporter_listen_host=None, interval=5, ), report_cmd=( 'printf "Subject: %s\nTo: %s\n\n%b" ' '"afancontrol daemon report: %REASON%" root "%MESSAGE%" | sendmail -t' ), triggers=TriggerConfig( global_commands=Actions( panic=AlertCommands(enter_cmd=None, leave_cmd=None), threshold=AlertCommands(enter_cmd=None, leave_cmd=None), ), temp_commands={ TempName("mobo"): Actions( panic=AlertCommands(enter_cmd=None, leave_cmd=None), threshold=AlertCommands(enter_cmd=None, leave_cmd=None), ) }, ), fans={}, readonly_fans={ ReadonlyFanName("cpu"): ReadonlyPWMFanNorm( fan_speed=LinuxFanSpeed( FanInputDevice("/sys/class/hwmon/hwmon0/device/fan1_input") ), pwm_read=LinuxFanPWMRead( PWMDevice("/sys/class/hwmon/hwmon0/device/pwm1") ), ) }, temps={ TempName("mobo"): FilteredTemp( temp=FileTemp( "/sys/class/hwmon/hwmon0/device/temp1_input", min=None, max=None, panic=None, threshold=None, ), filter=NullFilter(), ) }, mappings={}, )