diff --git a/README.md b/README.md index f57e56c..6f7fe46 100644 --- a/README.md +++ b/README.md @@ -20,17 +20,17 @@ Device #4: NZXT Kraken X (X42, X52, X62 or X72) # liquidctl status NZXT Smart Device (V1) -├── Fan 1 PWM -├── Fan 1 current 0.04 A -├── Fan 1 speed 1368 rpm +├── Fan 1 speed 1473 rpm ├── Fan 1 voltage 11.91 V -├── Fan 2 — -├── Fan 3 — -├── Firmware version 1.0.7 -├── LED accessories 2 -├── LED accessory type HUE+ Strip -├── LED count (total) 20 -└── Noise level 67 dB +├── Fan 1 current 0.01 A +├── Fan 1 control mode PWM +├── Fan 2 [...] +├── Fan 2 [...] +├── Firmware version 1.0.7 +├── LED accessories 2 +├── LED accessory type HUE+ Strip +├── LED count (total) 20 +└── Noise level 65 dB NZXT Kraken X (X42, X52, X62 or X72) ├── Liquid temperature 31.7 °C diff --git a/docs/corsair-commander-guide.md b/docs/corsair-commander-guide.md index fc34f23..30213ee 100644 --- a/docs/corsair-commander-guide.md +++ b/docs/corsair-commander-guide.md @@ -13,25 +13,25 @@ sensors and fan types are currently connected. ``` # liquidctl initialize Corsair Commander Pro (experimental) -├── Firmware version 0.9.212 -├── Bootloader version 0.5 -├── Temp sensor 1 Connected -├── Temp sensor 2 Connected -├── Temp sensor 3 Connected -├── Temp sensor 4 Connected -├── Fan 1 Mode DC -├── Fan 2 Mode DC -├── Fan 3 Mode DC -├── Fan 4 Mode Auto/Disconnected -├── Fan 5 Mode Auto/Disconnected -└── Fan 6 Mode Auto/Disconnected +├── Firmware version 0.9.212 +├── Bootloader version 0.5 +├── Temperature probe 1 Yes +├── Temperature probe 2 Yes +├── Temperature probe 3 No +├── Temperature probe 4 No +├── Fan 1 control mode PWM +├── Fan 2 control mode PWM +├── Fan 3 control mode DC +├── Fan 4 control mode N/A +├── Fan 5 control mode N/A +└── Fan 6 control mode N/A ``` ``` # liquidctl initialize Corsair Lighting Node Pro (experimental) -├── Firmware version 0.10.4 -└── Bootloader version 3.0 +├── Firmware version 0.10.4 +└── Bootloader version 3.0 ``` @@ -49,19 +49,14 @@ If a fan or temperature probe is not connected then a value of 0 is shown. ``` # liquidctl status Corsair Commander Pro (experimental) -├── 12 volt rail 12.06 V -├── 5 volt rail 4.96 V -├── 3.3 volt rail 3.36 V -├── Temp sensor 1 26.4 °C -├── Temp sensor 2 27.5 °C -├── Temp sensor 3 21.7 °C -├── Temp sensor 4 25.3 °C +├── Temperature 1 26.4 °C +├── Temperature 2 27.5 °C ├── Fan 1 speed 927 rpm ├── Fan 2 speed 927 rpm ├── Fan 3 speed 1195 rpm -├── Fan 4 speed 0 rpm -├── Fan 5 speed 0 rpm -└── Fan 6 speed 0 rpm +├── +12V rail 12.06 V +├── +5V rail 4.96 V +└── +3.3V rail 3.36 V ``` diff --git a/docs/nzxt-smart-device-v1-guide.md b/docs/nzxt-smart-device-v1-guide.md index c20d0f4..248b9c8 100644 --- a/docs/nzxt-smart-device-v1-guide.md +++ b/docs/nzxt-smart-device-v1-guide.md @@ -32,23 +32,23 @@ The device can report fan information for each channel, the noise level at the o ``` # liquidctl status NZXT Smart Device (V1) -├── Fan 1 PWM -├── Fan 1 current 0.04 A -├── Fan 1 speed 1064 rpm +├── Fan 1 speed 1473 rpm ├── Fan 1 voltage 11.91 V -├── Fan 2 PWM -├── Fan 2 current 0.01 A -├── Fan 2 speed 1051 rpm -├── Fan 2 voltage 11.77 V -├── Fan 3 PWM -├── Fan 3 current 0.09 A -├── Fan 3 speed 1581 rpm -├── Fan 3 voltage 11.77 V -├── Firmware version 1.0.7 -├── LED accessories 2 -├── LED accessory type HUE+ Strip -├── LED count (total) 20 -└── Noise level 62 dB +├── Fan 1 current 0.01 A +├── Fan 1 control mode PWM +├── Fan 2 speed 1341 rpm +├── Fan 2 voltage 11.91 V +├── Fan 2 current 0.02 A +├── Fan 2 control mode DC +├── Fan 3 speed 1352 rpm +├── Fan 3 voltage 11.91 V +├── Fan 3 current 0.02 A +├── Fan 3 control mode N/A +├── Firmware version 1.0.7 +├── LED accessories 2 +├── LED accessory type HUE+ Strip +├── LED count (total) 20 +└── Noise level 65 dB ``` diff --git a/liquidctl/cli.py b/liquidctl/cli.py index 1a277e2..5a167f8 100644 --- a/liquidctl/cli.py +++ b/liquidctl/cli.py @@ -200,7 +200,10 @@ def _print_dev_status(dev, status): for k, v, u in status: if isinstance(v, datetime.timedelta): v = str(v) - u = '' + elif isinstance(v, bool): + v = 'Yes' if v else 'No' + elif v is None: + v = 'N/A' else: valfmt = _VALUE_FORMATS.get(u, '') v = f'{v:{valfmt}}' diff --git a/liquidctl/driver/commander_pro.py b/liquidctl/driver/commander_pro.py index 3c5f782..696c1bb 100644 --- a/liquidctl/driver/commander_pro.py +++ b/liquidctl/driver/commander_pro.py @@ -99,18 +99,18 @@ def _quoted(*names): return ', '.join(map(repr, names)) -def _get_fan_mode_description(mode): +def _fan_mode_desc(mode): """This will convert the fan mode value to a descriptive name. """ - if mode == _FAN_MODE_DISCONNECTED: - return 'Auto/Disconnected' - elif mode == _FAN_MODE_DC: + if mode == _FAN_MODE_DC: return 'DC' elif mode == _FAN_MODE_PWM: return 'PWM' else: - return 'UNKNOWN' + if mode != _FAN_MODE_DISCONNECTED: + _LOGGER.warning('unknown fan mode: {mode:#04x}') + return None class CommanderPro(UsbHidDriver): @@ -178,10 +178,8 @@ class CommanderPro(UsbHidDriver): temp_connected = res[1:5] self._data.store('temp_sensors_connected', temp_connected) status += [ - ('Temp sensor 1', 'Connected' if temp_connected[0] else 'Not Connected', ''), - ('Temp sensor 2', 'Connected' if temp_connected[1] else 'Not Connected', ''), - ('Temp sensor 3', 'Connected' if temp_connected[2] else 'Not Connected', ''), - ('Temp sensor 4', 'Connected' if temp_connected[3] else 'Not Connected', ''), + (f'Temperature probe {i + 1}', bool(temp_connected[i]), '') + for i in range(4) ] if self._fan_count > 0: @@ -190,12 +188,8 @@ class CommanderPro(UsbHidDriver): fanModes = res[1:self._fan_count+1] self._data.store('fan_modes', fanModes) status += [ - ('Fan 1 Mode', _get_fan_mode_description(fanModes[0]), ''), - ('Fan 2 Mode', _get_fan_mode_description(fanModes[1]), ''), - ('Fan 3 Mode', _get_fan_mode_description(fanModes[2]), ''), - ('Fan 4 Mode', _get_fan_mode_description(fanModes[3]), ''), - ('Fan 5 Mode', _get_fan_mode_description(fanModes[4]), ''), - ('Fan 6 Mode', _get_fan_mode_description(fanModes[5]), ''), + (f'Fan {i + 1} control mode', _fan_mode_desc(fanModes[i]), '') + for i in range(6) ] return status @@ -210,42 +204,28 @@ class CommanderPro(UsbHidDriver): _LOGGER.debug('only the Commander Pro supports this') return [] - connected_temp_sensors = self._data.load('temp_sensors_connected', default=[0]*self._temp_probs) + temp_probes = self._data.load('temp_sensors_connected', default=[0]*self._temp_probs) fan_modes = self._data.load('fan_modes', default=[0]*self._fan_count) + status = [] + # get the temperature sensor values - temp = [0]*self._temp_probs - for num, enabled in enumerate(connected_temp_sensors): - if enabled: - temp[num] = self._get_temp(num) - - # get the real power supply voltages - res = self._send_command(_CMD_GET_VOLTS, [0]) - volt_12 = u16be_from(res, offset=1) / 1000 - - res = self._send_command(_CMD_GET_VOLTS, [1]) - volt_5 = u16be_from(res, offset=1) / 1000 - - res = self._send_command(_CMD_GET_VOLTS, [2]) - volt_3 = u16be_from(res, offset=1) / 1000 + for i, probe_enabled in enumerate(temp_probes): + if probe_enabled: + temp = self._get_temp(i) + status.append((f'Temperature {i + 1}', temp, '°C')) # get fan RPMs of connected fans - fanspeeds = [0]*self._fan_count - for fan_num, mode in enumerate(fan_modes): - if mode == _FAN_MODE_DC or mode == _FAN_MODE_PWM: - fanspeeds[fan_num] = self._get_fan_rpm(fan_num) + for i, fan_mode in enumerate(fan_modes): + if fan_mode == _FAN_MODE_DC or fan_mode == _FAN_MODE_PWM: + speed = self._get_fan_rpm(i) + status.append((f'Fan {i + 1} speed', speed, 'rpm')) - status = [ - ('12 volt rail', volt_12, 'V'), - ('5 volt rail', volt_5, 'V'), - ('3.3 volt rail', volt_3, 'V'), - ] - - for temp_num in range(self._temp_probs): - status += [(f'Temp sensor {temp_num + 1}', temp[temp_num], '°C')] - - for fan_num in range(self._fan_count): - status += [(f'Fan {fan_num + 1} speed', fanspeeds[fan_num], 'rpm')] + # get the real power supply voltages + for i, rail in enumerate(["+12V", "+5V", "+3.3V"]): + raw = self._send_command(_CMD_GET_VOLTS, [i]) + voltage = u16be_from(raw, offset=1) / 1000 + status.append((f'{rail} rail', voltage, 'V')) return status diff --git a/liquidctl/driver/smart_device.py b/liquidctl/driver/smart_device.py index bca66af..f664735 100644 --- a/liquidctl/driver/smart_device.py +++ b/liquidctl/driver/smart_device.py @@ -261,22 +261,29 @@ class SmartDevice(_CommonSmartDeviceDriver): """ status = [] + fans = [None] * len(self._speed_channels) noise = [] + self.device.clear_enqueued_reports() for i, _ in enumerate(self._speed_channels): msg = self.device.read(self._READ_LENGTH) num = (msg[15] >> 4) + 1 state = msg[15] & 0x3 - status.append((f'Fan {num}', ['—', 'DC', 'PWM'][state], '')) + + fans[num - 1] = [ + (f'Fan {num} speed', msg[3] << 8 | msg[4], 'rpm'), + (f'Fan {num} voltage', msg[7] + msg[8]/100, 'V'), + (f'Fan {num} current', msg[9] + msg[10]/100, 'A'), + (f'Fan {num} control mode', [None, 'DC', 'PWM'][state], ''), + ] noise.append(msg[1]) - if state: - status.append((f'Fan {num} speed', msg[3] << 8 | msg[4], 'rpm')) - status.append((f'Fan {num} voltage', msg[7] + msg[8]/100, 'V')) - status.append((f'Fan {num} current', msg[10]/100, 'A')) + if i != 0: continue + fw = '{}.{}.{}'.format(msg[0xb], msg[0xc] << 8 | msg[0xd], msg[0xe]) status.append(('Firmware version', fw, '')) + if self._color_channels: lcount = msg[0x11] status.append(('LED accessories', lcount, '')) @@ -284,8 +291,11 @@ class SmartDevice(_CommonSmartDeviceDriver): ltype, lsize = [('HUE+ Strip', 10), ('Aer RGB', 8)][msg[0x10] >> 3] status.append(('LED accessory type', ltype, '')) status.append(('LED count (total)', lcount*lsize, '')) + status.append(('Noise level', round(sum(noise)/len(noise)), 'dB')) - return sorted(status) + + # flatten non None fan data and concat with status + return [x for fan_data in fans if fan_data for x in fan_data] + status def _write_colors(self, cid, mode, colors, sval, direction='forward'): mval, mod3, mod4, _, _ = self._COLOR_MODES[mode] diff --git a/tests/test_commander_pro.py b/tests/test_commander_pro.py index 49b960a..85e6087 100644 --- a/tests/test_commander_pro.py +++ b/tests/test_commander_pro.py @@ -1,7 +1,7 @@ import pytest from _testutils import MockHidapiDevice, Report, MockRuntimeStorage -from liquidctl.driver.commander_pro import _quoted, _prepare_profile, _get_fan_mode_description, CommanderPro +from liquidctl.driver.commander_pro import _quoted, _prepare_profile, _fan_mode_desc, CommanderPro from liquidctl.error import NotSupportedByDevice @@ -117,22 +117,22 @@ def test_quoted_not_string(): # fan modes def test_get_fan_mode_description_auto(): - assert _get_fan_mode_description(0x00) == 'Auto/Disconnected' + assert _fan_mode_desc(0x00) == None def test_get_fan_mode_description_unknown(): - assert _get_fan_mode_description(0x03) == 'UNKNOWN' - assert _get_fan_mode_description(0x04) == 'UNKNOWN' - assert _get_fan_mode_description(0x10) == 'UNKNOWN' - assert _get_fan_mode_description(0xff) == 'UNKNOWN' + assert _fan_mode_desc(0x03) == None + assert _fan_mode_desc(0x04) == None + assert _fan_mode_desc(0x10) == None + assert _fan_mode_desc(0xff) == None def test_get_fan_mode_description_dc(): - assert _get_fan_mode_description(0x01) == 'DC' + assert _fan_mode_desc(0x01) == 'DC' def test_get_fan_mode_description_pwm(): - assert _get_fan_mode_description(0x02) == 'PWM' + assert _fan_mode_desc(0x02) == 'PWM' # class methods @@ -177,20 +177,20 @@ def test_initialize_commander_pro(commanderProDevice): res = commanderProDevice.initialize() assert len(res) == 12 - assert res[0][1] == '0.9.212' - assert res[1][1] == '0.5' + assert res[0] == ('Firmware version', '0.9.212', '') + assert res[1] == ('Bootloader version', '0.5', '') - assert res[2][1] == 'Connected' - assert res[3][1] == 'Connected' - assert res[4][1] == 'Not Connected' - assert res[5][1] == 'Connected' + assert res[2] == ('Temperature probe 1', True, '') + assert res[3] == ('Temperature probe 2', True, '') + assert res[4] == ('Temperature probe 3', False, '') + assert res[5] == ('Temperature probe 4', True, '') - assert res[6][1] == 'DC' - assert res[7][1] == 'DC' - assert res[8][1] == 'PWM' - assert res[9][1] == 'Auto/Disconnected' - assert res[10][1] == 'Auto/Disconnected' - assert res[11][1] == 'Auto/Disconnected' + assert res[6] == ('Fan 1 control mode', 'DC', '') + assert res[7] == ('Fan 2 control mode', 'DC', '') + assert res[8] == ('Fan 3 control mode', 'PWM', '') + assert res[9] == ('Fan 4 control mode', None, '') + assert res[10] == ('Fan 5 control mode', None, '') + assert res[11] == ('Fan 6 control mode', None, '') data = commanderProDevice._data.load('fan_modes', None) assert data is not None @@ -238,12 +238,12 @@ def test_get_status_commander_pro(commanderProDevice): '000a8300000000000000000000000000', # temp sensor 1 '000b6a00000000000000000000000000', # temp sensor 2 '000a0e00000000000000000000000000', # temp sensor 4 + '0003ac00000000000000000000000000', # fan speed 1 + '0003ab00000000000000000000000000', # fan speed 2 + '0003db00000000000000000000000000', # fan speed 3 '002f2200000000000000000000000000', # get 12v '00136500000000000000000000000000', # get 5v '000d1f00000000000000000000000000', # get 3.3v - '0003ac00000000000000000000000000', # fan speed 1 - '0003ab00000000000000000000000000', # fan speed 2 - '0003db00000000000000000000000000' # fan speed 3 ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) @@ -252,27 +252,24 @@ def test_get_status_commander_pro(commanderProDevice): commanderProDevice._data.store('temp_sensors_connected', [0x01, 0x01, 0x00, 0x01]) res = commanderProDevice.get_status() + print(res) - assert len(res) == 13 - - # voltages - assert res[0][1] == 12.066 # 12v - assert res[1][1] == 4.965 # 5v - assert res[2][1] == 3.359 # 3.3v + assert len(res) == 9 # temp probes - assert res[3][1] == 26.91 - assert res[4][1] == 29.22 - assert res[5][1] == 0.0 - assert res[6][1] == 25.74 + assert res[0] == ('Temperature 1', 26.91, '°C') + assert res[1] == ('Temperature 2', 29.22, '°C') + assert res[2] == ('Temperature 4', 25.74, '°C') # fans rpm - assert res[7][1] == 940 - assert res[8][1] == 939 - assert res[9][1] == 987 - assert res[10][1] == 0 - assert res[11][1] == 0 - assert res[12][1] == 0 + assert res[3] == ('Fan 1 speed', 940, 'rpm') + assert res[4] == ('Fan 2 speed', 939, 'rpm') + assert res[5] == ('Fan 3 speed', 987, 'rpm') + + # voltages + assert res[6] == ('+12V rail', 12.066, 'V') + assert res[7] == ('+5V rail', 4.965, 'V') + assert res[8] == ('+3.3V rail', 3.359, 'V') # check the commands sent sent = commanderProDevice.device.sent @@ -282,13 +279,13 @@ def test_get_status_commander_pro(commanderProDevice): assert sent[1].data[0] == 0x11 assert sent[2].data[0] == 0x11 - assert sent[3].data[0] == 0x12 - assert sent[4].data[0] == 0x12 - assert sent[5].data[0] == 0x12 + assert sent[3].data[0] == 0x21 + assert sent[4].data[0] == 0x21 + assert sent[5].data[0] == 0x21 - assert sent[6].data[0] == 0x21 - assert sent[7].data[0] == 0x21 - assert sent[8].data[0] == 0x21 + assert sent[6].data[0] == 0x12 + assert sent[7].data[0] == 0x12 + assert sent[8].data[0] == 0x12 def test_get_status_lighting_pro(lightingNodeProDevice): diff --git a/tests/test_smart_device.py b/tests/test_smart_device.py index db7e4bf..b2d8185 100644 --- a/tests/test_smart_device.py +++ b/tests/test_smart_device.py @@ -3,6 +3,12 @@ from _testutils import MockHidapiDevice, Report from liquidctl.driver.smart_device import SmartDevice +SAMPLE_RESPONSES = [ + '043e00056e00000b5b000301000007200002001e00', + '04400005b500000b5b000201000007020002001e00', + '044000053800000b5b000201000007120102001e00', +] + @pytest.fixture def mockSmartDevice(): @@ -35,3 +41,38 @@ def test_smart_device_not_totally_broken(mockSmartDevice): speed='fastest') dev.set_fixed_speed(channel='fan3', duty=50) + + +def test_smart_device_reads_status(mockSmartDevice): + dev = mockSmartDevice + + for _, capdata in enumerate(SAMPLE_RESPONSES): + capdata = bytes.fromhex(capdata) + dev.device.preload_read(Report(capdata[0], capdata[1:])) + + # skip initialize for now, we're not emulating the behavior precisely + # enough to require it here + + expected = [ + ('Fan 1 speed', 1461, 'rpm'), + ('Fan 1 voltage', 11.91, 'V'), + ('Fan 1 current', 0.02, 'A'), + ('Fan 1 control mode', 'PWM', ''), + ('Fan 2 speed', 1336, 'rpm'), + ('Fan 2 voltage', 11.91, 'V'), + ('Fan 2 current', 0.02, 'A'), + ('Fan 2 control mode', 'PWM', ''), + ('Fan 3 speed', 1390, 'rpm'), + ('Fan 3 voltage', 11.91, 'V'), + ('Fan 3 current', 0.03, 'A'), + ('Fan 3 control mode', None, ''), + ('Firmware version', '1.0.7', ''), + ('LED accessories', 2, ''), + ('LED accessory type', 'HUE+ Strip', ''), + ('LED count (total)', 20, ''), + ('Noise level', 63, 'dB') + ] + + got = dev.get_status() + + assert expected == got