Beispiel #1
0
def pwmfan_norm(pwmfan):
    return PWMFanNorm(
        pwmfan,
        pwm_line_start=PWMValue(100),
        pwm_line_end=PWMValue(240),
        never_stop=False,
    )
def pwmfan_norm(fan_speed, pwm_read, pwm_write):
    return PWMFanNorm(
        fan_speed,
        pwm_read,
        pwm_write,
        pwm_line_start=PWMValue(100),
        pwm_line_end=PWMValue(240),
        never_stop=False,
    )
Beispiel #3
0
def test_smoke(dummy_arduino):
    conn = ArduinoConnection(ArduinoName("test"), dummy_arduino.pyserial_url)
    fan = ArduinoPWMFan(conn, pwm_pin=ArduinoPin(9), tacho_pin=ArduinoPin(3))

    dummy_arduino.set_inner_state_pwms({"9": 42})

    with ExitStack() as stack:
        assert not dummy_arduino.is_connected
        stack.enter_context(fan)
        dummy_arduino.accept()
        assert dummy_arduino.is_connected

        dummy_arduino.set_speeds({"3": 1200})
        conn.wait_for_status(
        )  # required only for synchronization in the tests
        assert fan.get_speed() == 1200
        assert fan.get() == 255
        assert dummy_arduino.inner_state_pwms["9"] == 255

        fan.set(PWMValue(192))
        dummy_arduino.set_speeds({"3": 998})
        conn.wait_for_status(
        )  # required only for synchronization in the tests
        assert fan.get_speed() == 998
        assert fan.get() == 192
        assert dummy_arduino.inner_state_pwms["9"] == 192

    dummy_arduino.wait_for_disconnected()
    assert dummy_arduino.inner_state_pwms["9"] == 255
    assert not dummy_arduino.is_connected
    dummy_arduino.ensure_no_errors_in_thread()
Beispiel #4
0
    def from_configparser(
        cls,
        section: ConfigParserSection[FanName],
        arduino_connections: Mapping[ArduinoName, ArduinoConnection],
    ) -> "PWMFanNorm":
        readwrite_fan = ReadWriteFan.from_configparser(section, arduino_connections)
        never_stop = section.getboolean("never_stop", fallback=True)
        pwm_line_start = PWMValue(section.getint("pwm_line_start", fallback=100))
        pwm_line_end = PWMValue(section.getint("pwm_line_end", fallback=240))

        for pwm_value in (pwm_line_start, pwm_line_end):
            if not (
                readwrite_fan.pwm_read.min_pwm
                <= pwm_value
                <= readwrite_fan.pwm_read.max_pwm
            ):
                raise RuntimeError(
                    "Incorrect PWM value '%s' for fan '%s': it must be within [%s;%s]"
                    % (
                        pwm_value,
                        section.name,
                        readwrite_fan.pwm_read.min_pwm,
                        readwrite_fan.pwm_read.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'"
                % (section.name,)
            )

        return cls(
            readwrite_fan.fan_speed,
            readwrite_fan.pwm_read,
            readwrite_fan.pwm_write,
            pwm_line_start=pwm_line_start,
            pwm_line_end=pwm_line_end,
            never_stop=never_stop,
        )
Beispiel #5
0
def run_fantest(
    fan: ReadWriteFan, pwm_step_size: PWMValue, output: "MeasurementsOutput"
) -> None:
    with fan.fan_speed, fan.pwm_read, fan.pwm_write:
        start = fan.pwm_read.min_pwm
        stop = fan.pwm_read.max_pwm
        if pwm_step_size > 0:
            print("Testing increase with step %s" % pwm_step_size)
            print("Waiting %s seconds for fan to stop..." % FAN_RESET_INTERVAL_SECONDS)
        else:
            start, stop = stop, start
            print("Testing decrease with step %s" % pwm_step_size)
            print(
                "Waiting %s seconds for fan to run in full speed..."
                % FAN_RESET_INTERVAL_SECONDS
            )

        fan.pwm_write.set(start)
        sleep(FAN_RESET_INTERVAL_SECONDS)

        print(output.header())

        prev_rpm = None
        for pwm_value in range(start, stop, pwm_step_size):
            fan.pwm_write.set(PWMValue(pwm_value))
            sleep(STEP_INTERVAL_SECONDS)
            rpm = fan.fan_speed.get_speed()

            rpm_delta = None  # Optional[FanValue]
            if prev_rpm is not None:
                rpm_delta = rpm - prev_rpm
            prev_rpm = rpm

            print(
                output.data_row(pwm=PWMValue(pwm_value), rpm=rpm, rpm_delta=rpm_delta)
            )

        print("Test is complete, returning fan to full speed")
Beispiel #6
0
    def set(self, pwm_norm: PWMValueNorm) -> PWMValue:
        # TODO validate this formula
        pwm_norm = max(pwm_norm, PWMValueNorm(0.0))
        pwm_norm = min(pwm_norm, PWMValueNorm(1.0))
        pwm = pwm_norm * self.pwm_line_end
        if 0 < pwm < self.pwm_line_start:
            pwm = self.pwm_line_start
        if pwm <= 0 and self.never_stop:
            pwm = self.pwm_line_start
        if pwm_norm >= 1.0:
            pwm = self.pwm_read.max_pwm

        pwm = PWMValue(int(math.ceil(pwm)))
        self.pwm_write.set(pwm)
        return pwm
Beispiel #7
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)
Beispiel #8
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
Beispiel #9
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)],
            )
        },
    )
Beispiel #10
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)],
            )
        },
    )
Beispiel #11
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)],
            ),
        },
    )
Beispiel #12
0
 def get(self) -> PWMValue:
     return PWMValue(int(self._conn.get_pwm(self._pwm_pin)))
Beispiel #13
0
 def parse(cls, b: bytes) -> "SetPWMCommand":
     command, pwm_pin, pwm = struct.unpack("sBB", b)
     if command != cls.command:
         raise ValueError("Invalid command marker. Expected %r, got %r" %
                          (cls.command, command))
     return cls(pwm_pin=ArduinoPin(pwm_pin), pwm=PWMValue(pwm))