示例#1
0
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)
示例#2
0
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
示例#3
0
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
示例#4
0
def pwm_read(pwm_path):
    return LinuxFanPWMRead(pwm=PWMDevice(str(pwm_path)))
示例#5
0
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)
示例#6
0
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
示例#7
0
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)],
            )
        },
    )
示例#8
0
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)],
            )
        },
    )
示例#9
0
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)],
            ),
        },
    )
示例#10
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={},
    )