def test_triggers_good_temp(report): 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), ) }, ), report=report, ) with t: assert not t.is_alerting t.check( { TempName("mobo"): TempStatus( temp=TempCelsius(34.0), min=TempCelsius(40.0), max=TempCelsius(50.0), panic=TempCelsius(60.0), threshold=None, is_panic=False, is_threshold=False, ) } ) assert not t.is_alerting
def make_temp_status(temp): return TempStatus( min=TempCelsius(30), max=TempCelsius(50), temp=TempCelsius(temp), panic=None, threshold=None, is_panic=False, is_threshold=False, )
def test_hddtemp_exec_failed(): t = HDDTemp( disk_path="/dev/sd?", min=TempCelsius(38.0), max=TempCelsius(45.0), panic=TempCelsius(50.0), threshold=None, hddtemp_bin="false", ) with pytest.raises(subprocess.CalledProcessError): t._call_hddtemp()
def test_bad_temp(cls, report, sense_exec_shell_command): t = cls( global_commands=AlertCommands( enter_cmd="printf '@%s' enter", leave_cmd="printf '@%s' leave" ), temp_commands=dict( mobo=AlertCommands( enter_cmd="printf '@%s' mobo enter", leave_cmd="printf '@%s' mobo leave" ) ), report=report, ) with sense_exec_shell_command(trigger) as (mock_exec_shell_command, get_stdout): with t: assert not t.is_alerting t.check( dict( mobo=TempStatus( temp=TempCelsius(70.0), min=TempCelsius(40.0), max=TempCelsius(50.0), panic=TempCelsius(60.0), threshold=TempCelsius(55.0), is_panic=True, is_threshold=True, ) ) ) assert t.is_alerting assert mock_exec_shell_command.call_args_list == [ call("printf '@%s' mobo enter"), call("printf '@%s' enter"), ] assert ["@mobo@enter", "@enter"] == get_stdout() mock_exec_shell_command.reset_mock() t.check( dict( mobo=TempStatus( temp=TempCelsius(34.0), min=TempCelsius(40.0), max=TempCelsius(50.0), panic=TempCelsius(60.0), threshold=None, is_panic=False, is_threshold=False, ) ) ) assert not t.is_alerting assert mock_exec_shell_command.call_args_list == [ call("printf '@%s' mobo leave"), call("printf '@%s' leave"), ] assert ["@mobo@leave", "@leave"] == get_stdout() mock_exec_shell_command.reset_mock() assert 0 == mock_exec_shell_command.call_count
def test_hddtemp_bad(hddtemp_output_bad): with patch.object(HDDTemp, "_call_hddtemp") as mock_call_hddtemp: mock_call_hddtemp.return_value = hddtemp_output_bad t = HDDTemp( disk_path="/dev/sda", min=TempCelsius(38.0), max=TempCelsius(45.0), panic=TempCelsius(50.0), threshold=None, hddtemp_bin="testbin", ) with pytest.raises(RuntimeError): t.get()
def test_hddtemp_exec_successful(temp_path): (temp_path / "sda").write_text("") (temp_path / "sdz").write_text("") t = HDDTemp( disk_path=str(temp_path / "sd") + "?", min=TempCelsius(38.0), max=TempCelsius(45.0), panic=TempCelsius(50.0), threshold=None, hddtemp_bin="printf '@%s'", ) expected_out = "@-n@-u@C@--@{0}/sda@{0}/sdz".format(temp_path) assert expected_out == t._call_hddtemp()
def test_command_temp_without_minmax(): t = CommandTemp( shell_command=r"printf '%s\n' 35 30 40", min=None, max=None, panic=TempCelsius(50.0), threshold=None, ) assert t.get() == TempStatus( temp=TempCelsius(35.0), min=TempCelsius(30.0), max=TempCelsius(40.0), panic=TempCelsius(50.0), threshold=None, is_panic=False, is_threshold=False, )
def test_file_temp_min_max_files(temp_path, file_temp_path): with pytest.raises(RuntimeError): # min == max is an error FileTemp( temp_path=str(file_temp_path), min=None, max=None, panic=TempCelsius(60.0), threshold=None, ).get() temp = FileTemp( temp_path=str(file_temp_path), min=TempCelsius(50.0), max=None, panic=TempCelsius(60.0), threshold=None, ) assert temp.get() == TempStatus( temp=TempCelsius(34.0), min=TempCelsius(50.0), max=TempCelsius(127.0), panic=TempCelsius(60.0), threshold=None, is_panic=False, is_threshold=False, )
def test_file_temp_glob(file_temp_path): temp = FileTemp( temp_path=str(file_temp_path).replace("/temp1", "/temp?"), min=TempCelsius(40.0), max=None, panic=None, threshold=None, ) assert temp.get() == TempStatus( temp=TempCelsius(34.0), min=TempCelsius(40.0), max=TempCelsius(127.0), panic=None, threshold=None, is_panic=False, is_threshold=False, ) print(repr(temp))
def test_good_temp(cls, report): t = cls( global_commands=AlertCommands(enter_cmd=None, leave_cmd=None), temp_commands=dict(mobo=AlertCommands(enter_cmd=None, leave_cmd=None)), report=report, ) with t: assert not t.is_alerting t.check( dict( mobo=TempStatus( temp=TempCelsius(34.0), min=TempCelsius(40.0), max=TempCelsius(50.0), panic=TempCelsius(60.0), threshold=None, is_panic=False, is_threshold=False, ) ) ) assert not t.is_alerting
def test_temp( temp: TempCelsius, threshold: Optional[TempCelsius], panic: TempCelsius, is_threshold, is_panic, ): min = TempCelsius(40.0) max = TempCelsius(50.0) with patch.object(DummyTemp, "_get_temp") as mock_get_temp: t = DummyTemp(panic=panic, threshold=threshold) mock_get_temp.return_value = [temp, min, max] assert t.get() == TempStatus( temp=temp, min=min, max=max, panic=panic, threshold=threshold, is_panic=is_panic, is_threshold=is_threshold, )
def test_file_temp_min_max_numbers(file_temp_path): temp = FileTemp( temp_path=str(file_temp_path), min=TempCelsius(40.0), max=TempCelsius(50.0), panic=TempCelsius(60.0), threshold=None, ) assert temp.get() == TempStatus( temp=TempCelsius(34.0), min=TempCelsius(40.0), max=TempCelsius(50.0), panic=TempCelsius(60.0), threshold=None, is_panic=False, is_threshold=False, ) print(repr(temp))
def test_hddtemp_many(hddtemp_output_many): with patch.object(HDDTemp, "_call_hddtemp") as mock_call_hddtemp: mock_call_hddtemp.return_value = hddtemp_output_many t = HDDTemp( disk_path="/dev/sd?", min=TempCelsius(38.0), max=TempCelsius(45.0), panic=TempCelsius(50.0), threshold=None, hddtemp_bin="testbin", ) assert t.get() == TempStatus( temp=TempCelsius(39.0), min=TempCelsius(38.0), max=TempCelsius(45.0), panic=TempCelsius(50.0), threshold=None, is_panic=False, is_threshold=False, ) print(repr(t))
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_prometheus_metrics(requests_session): mocked_fan = MagicMock(spec=PWMFanNorm)() mocked_triggers = MagicMock(spec=Triggers)() mocked_report = MagicMock(spec=Report)() port = random.randint(20000, 50000) metrics = PrometheusMetrics("127.0.0.1:%s" % port) with metrics: resp = requests_session.get("http://127.0.0.1:%s/metrics" % port) assert resp.status_code == 200 assert "is_threshold 0.0" in resp.text with metrics.measure_tick(): sleep(0.01) resp = requests_session.get("http://127.0.0.1:%s/metrics" % port) assert resp.status_code == 200 assert "tick_duration_count 1.0" in resp.text assert "tick_duration_sum 0." in resp.text mocked_triggers.panic_trigger.is_alerting = True mocked_triggers.threshold_trigger.is_alerting = False mocked_fan.pwm_line_start = 100 mocked_fan.pwm_line_end = 240 mocked_fan.get_speed.return_value = 999 mocked_fan.get_raw.return_value = 142 mocked_fan.get = types.MethodType(PWMFanNorm.get, mocked_fan) mocked_fan.pwmfan.max_pwm = 255 metrics.tick( temps={ TempName("goodtemp"): TempStatus( temp=TempCelsius(74.0), min=TempCelsius(40.0), max=TempCelsius(50.0), panic=TempCelsius(60.0), threshold=None, is_panic=True, is_threshold=False, ), TempName("failingtemp"): None, }, fans=Fans(fans={FanName("test"): mocked_fan}, report=mocked_report), triggers=mocked_triggers, ) resp = requests_session.get("http://127.0.0.1:%s/metrics" % port) assert resp.status_code == 200 print(resp.text) assert 'temperature_current{temp_name="failingtemp"} NaN' in resp.text assert 'temperature_current{temp_name="goodtemp"} 74.0' in resp.text assert 'temperature_is_failing{temp_name="failingtemp"} 1.0' in resp.text assert 'temperature_is_failing{temp_name="goodtemp"} 0.0' in resp.text assert 'fan_rpm{fan_name="test"} 999.0' in resp.text assert 'fan_pwm{fan_name="test"} 142.0' in resp.text assert 'fan_pwm_normalized{fan_name="test"} 0.556' in resp.text assert 'fan_is_failing{fan_name="test"} 0.0' in resp.text assert "is_panic 1.0" in resp.text assert "is_threshold 0.0" in resp.text assert "last_metrics_tick_seconds_ago 0." in resp.text with pytest.raises(IOError): requests_session.get("http://127.0.0.1:%s/metrics" % port)
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)], ) }, )
assert mocked_triggers.check.call_count == 1 assert mocked_case_fan.__enter__.call_count == 1 assert mocked_metrics.__enter__.call_count == 1 assert mocked_metrics.tick.call_count == 1 assert mocked_case_fan.__exit__.call_count == 1 assert mocked_metrics.__exit__.call_count == 1 @pytest.mark.parametrize( "temps, mappings, expected_fan_speeds", [ ( { TempName("cpu"): TempStatus( min=TempCelsius(30), max=TempCelsius(50), temp=TempCelsius((50 - 30) * 0.42 + 30), panic=None, threshold=None, is_panic=False, is_threshold=False, ), TempName("hdd"): None, # a failing sensor }, { MappingName("all"): FansTempsRelation( temps=[TempName("cpu"), TempName("hdd")], fans=[FanSpeedModifier(fan=FanName("rear"), modifier=1.0)],
def _parse_temps( config: configparser.ConfigParser, hddtemp: str ) -> Tuple[Mapping[TempName, Temp], Mapping[TempName, Actions]]: temps = {} # type: Dict[TempName, Temp] temp_commands = {} # type: Dict[TempName, Actions] for section_name in config.sections(): section_name_parts = section_name.split(":", 1) if section_name_parts[0].strip().lower() != "temp": continue temp_name = TempName(section_name_parts[1].strip()) temp = config[section_name] keys = set(temp.keys()) actions_panic = AlertCommands( enter_cmd=first_not_none(temp.get("panic_enter_cmd")), leave_cmd=first_not_none(temp.get("panic_leave_cmd")), ) keys.discard("panic_enter_cmd") keys.discard("panic_leave_cmd") actions_threshold = AlertCommands( enter_cmd=first_not_none(temp.get("threshold_enter_cmd")), leave_cmd=first_not_none(temp.get("threshold_leave_cmd")), ) keys.discard("threshold_enter_cmd") keys.discard("threshold_leave_cmd") panic = TempCelsius(temp.getfloat("panic")) threshold = TempCelsius(temp.getfloat("threshold")) min = TempCelsius(temp.getfloat("min")) max = TempCelsius(temp.getfloat("max")) keys.discard("panic") keys.discard("threshold") keys.discard("min") keys.discard("max") type = temp["type"] keys.discard("type") if type == "file": t = FileTemp(temp["path"], min=min, max=max, panic=panic, threshold=threshold) # type: Temp keys.discard("path") elif type == "hdd": if min is None or max is None: raise RuntimeError( "hdd temp '%s' doesn't define the mandatory `min` and `max` temps" % temp_name) t = HDDTemp( temp["path"], min=min, max=max, panic=panic, threshold=threshold, hddtemp_bin=hddtemp, ) keys.discard("path") elif type == "exec": t = CommandTemp(temp["command"], min=min, max=max, panic=panic, threshold=threshold) keys.discard("command") else: raise RuntimeError("Unsupported temp type '%s' for temp '%s'" % (type, temp_name)) if keys: raise RuntimeError("Unknown options in the [%s] section: %s" % (section_name, keys)) if temp_name in temps: raise RuntimeError("Duplicate temp section declaration for '%s'" % temp_name) temps[temp_name] = t temp_commands[temp_name] = Actions(panic=actions_panic, threshold=actions_threshold) if not temps: raise RuntimeError( "No temps found in the config, at least 1 must be specified") return temps, temp_commands