Support for NZXT H1 V2 case Smart Device (#451)
This commit is contained in:
parent
3e1d3a4115
commit
b66268e388
|
@ -134,6 +134,7 @@ The notes are sorted alphabetically, major (upper case) notes before minor
|
|||
| Fan/LED controller | [NZXT RGB & Fan Controller](docs/nzxt-hue2-guide.md) | USB HID | <sup>_h_</sup> |
|
||||
| Fan/LED controller | [NZXT Smart Device](docs/nzxt-smart-device-v1-guide.md) | USB HID | <sup>_h_</sup> |
|
||||
| Fan/LED controller | [NZXT Smart Device V2](docs/nzxt-hue2-guide.md) | USB HID | <sup>_h_</sup> |
|
||||
| Fan/AIO controller | [NZXT H1 V2](docs/nzxt-hue2-guide.md) | USB HID | |
|
||||
| Graphics card RGB | [ASUS Strix GTX 1070 OC](docs/nvidia-guide.md) | I²C | <sup>_Ux_</sup> |
|
||||
| Graphics card RGB | [ASUS Strix RTX 2080 Ti OC](docs/nvidia-guide.md) | I²C | <sup>_Ux_</sup> |
|
||||
| Graphics card RGB | [Additional ASUS GTX and RTX cards](docs/nvidia-guide.md) | I²C | <sup>_Uex_</sup> |
|
||||
|
|
|
@ -26,6 +26,12 @@ It provides two HUE 2 lighting channels and three independent fan channels with
|
|||
|
||||
A microphone is still present onboard for noise level optimization through CAM and AI.
|
||||
|
||||
## NZXT H1 V2
|
||||
|
||||
The second revision of the NZXT H1 case, labeled H1 V2, ships with a variant of the NZXT Smart Device V2 that handles both the internal fans and the AIO pump. Two fan and one pump channels are available,
|
||||
where the formers can be controlled via PWM or DC. The pump speed is not user controllable. The device reports the state, speed and duty of each fan channel, as well as the pump speed.
|
||||
|
||||
There are no lighting channels available nor an onboard microphone.
|
||||
|
||||
## NZXT RGB & Fan Controller
|
||||
|
||||
|
@ -70,7 +76,7 @@ NZXT Smart Device V2
|
|||
|
||||
## Fan speeds
|
||||
|
||||
_Only NZXT Smart Device V2_
|
||||
_Only NZXT Smart Device V2 and NZXT H1 V2_
|
||||
|
||||
Fan speeds can only be set to fixed duty values.
|
||||
|
||||
|
@ -79,10 +85,10 @@ Fan speeds can only be set to fixed duty values.
|
|||
```
|
||||
|
||||
| Channel | Minimum duty | Maximum duty | Note |
|
||||
| --- | --- | --- | - |
|
||||
| fan1 | 0% | 100% ||
|
||||
| fan2 | 0% | 100% ||
|
||||
| fan3 | 0% | 100% ||
|
||||
| --- | --- | --- | --- |
|
||||
| fan1 | 0% | 100% | H1 V2: GPU exhaust fan |
|
||||
| fan2 | 0% | 100% | H1 V2: AIO fan |
|
||||
| fan3 | 0% | 100% | H1 V2: not available |
|
||||
| sync | 0% | 100% | all available channels |
|
||||
|
||||
*Always check that the settings are appropriate for the use case, and that they correctly apply and persist.*
|
||||
|
|
|
@ -472,3 +472,6 @@ SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="200d", TAG+="uacc
|
|||
|
||||
# NZXT Smart Device V2
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="200f", TAG+="uaccess"
|
||||
|
||||
# NZXT H1 V2
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="2015", TAG+="uaccess"
|
||||
|
|
|
@ -55,6 +55,18 @@ lighting channel supports up to 6 accessories and a total of 40 LEDs.
|
|||
A microphone is still present onboard for noise level optimization through CAM
|
||||
and AI.
|
||||
|
||||
NZXT H1 V2
|
||||
----------
|
||||
|
||||
The second revision of the NZXT H1 case, labeled H1 V2, ships with a variant of the
|
||||
NZXT Smart Device V2 that handles both the internal fans and the AIO pump. Two fan and
|
||||
one pump channels are available, where the formers can be controlled via PWM or DC.
|
||||
|
||||
The pump speed is not user controllable. The device reports the state, speed and duty
|
||||
of each fan channel, as well as the pump speed.
|
||||
|
||||
There are no lighting channels available nor an onboard microphone.
|
||||
|
||||
RGB & Fan Controller
|
||||
--------------------
|
||||
|
||||
|
@ -466,7 +478,8 @@ class SmartDevice2(_BaseSmartDevice):
|
|||
for i in range(speed_channel_count)}
|
||||
color_channels = {f'led{i + 1}': (1 << i)
|
||||
for i in range(color_channel_count)}
|
||||
color_channels['sync'] = (1 << color_channel_count) - 1
|
||||
if color_channels:
|
||||
color_channels['sync'] = (1 << color_channel_count) - 1
|
||||
super().__init__(device, description, speed_channels, color_channels, **kwargs)
|
||||
|
||||
def initialize(self, direct_access=False, **kwargs):
|
||||
|
@ -518,7 +531,10 @@ class SmartDevice2(_BaseSmartDevice):
|
|||
ret.append((f'LED {c + 1} accessory {a + 1}',
|
||||
Hue2Accessory(accessory_id), ''))
|
||||
|
||||
self._read_until({b'\x11\x01': parse_firm_info, b'\x21\x03': parse_led_info})
|
||||
parsers = {b'\x11\x01': parse_firm_info}
|
||||
if self._color_channels:
|
||||
parsers[b'\x21\x03'] = parse_led_info
|
||||
self._read_until(parsers)
|
||||
return sorted(ret)
|
||||
|
||||
def _get_status_directly(self):
|
||||
|
@ -589,6 +605,9 @@ class SmartDevice2(_BaseSmartDevice):
|
|||
def _write_colors(self, cid, mode, colors, sval, direction='forward',):
|
||||
mval, mod3, mod4, mincolors, maxcolors = self._COLOR_MODES[mode]
|
||||
|
||||
if not self._color_channels:
|
||||
raise NotSupportedByDevice()
|
||||
|
||||
color_count = len(colors)
|
||||
if maxcolors == 40:
|
||||
led_padding = [0x00, 0x00, 0x00]*(maxcolors - color_count) # turn off remaining LEDs
|
||||
|
@ -627,6 +646,39 @@ class SmartDevice2(_BaseSmartDevice):
|
|||
self._write(msg)
|
||||
|
||||
|
||||
class H1V2(SmartDevice2):
|
||||
SUPPORTED_DEVICES = [
|
||||
(0x1e71, 0x2015, None, 'NZXT H1 V2', {
|
||||
'speed_channel_count': 2,
|
||||
'color_channel_count': 0
|
||||
}),
|
||||
]
|
||||
|
||||
def get_status(self, direct_access=False, **kwargs):
|
||||
ret = []
|
||||
|
||||
def parse_fan_info(msg):
|
||||
mode_offset = 21
|
||||
rpm_offset = 24
|
||||
duty_offset = 25
|
||||
pump_offset = 18
|
||||
raw_modes = [None, 'DC', 'PWM']
|
||||
|
||||
for i, _ in enumerate(self._speed_channels):
|
||||
mode = raw_modes[msg[mode_offset + i]]
|
||||
ret.append((f'Fan {i + 1} speed', msg[rpm_offset] << 8 | msg[rpm_offset - 1], 'rpm'))
|
||||
ret.append((f'Fan {i + 1} duty', msg[duty_offset], '%'))
|
||||
ret.append((f'Fan {i + 1} control mode', mode, ''))
|
||||
rpm_offset += 5
|
||||
duty_offset += 5
|
||||
ret.append(('Pump speed', msg[pump_offset] << 8 | msg[pump_offset - 1], 'rpm'))
|
||||
|
||||
# parse fans and pump status
|
||||
self.device.clear_enqueued_reports()
|
||||
self._read_until({b'\x75\x02': parse_fan_info})
|
||||
return sorted(ret)
|
||||
|
||||
|
||||
# backward compatibility
|
||||
NzxtSmartDeviceDriver = SmartDevice
|
||||
SmartDeviceDriver = SmartDevice
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
# uses the psf/black style
|
||||
|
||||
import pytest
|
||||
from _testutils import MockHidapiDevice, Report
|
||||
|
||||
from liquidctl.driver.hwmon import HwmonDevice
|
||||
from liquidctl.driver.smart_device import H1V2
|
||||
|
||||
SAMPLE_STATUS = bytes.fromhex(
|
||||
"75021320020d85bcabab94188f5f010000a00f0032020284021e1e02f9066464"
|
||||
"0000000000000000000000000000000000000000000000000000000000000005"
|
||||
)
|
||||
|
||||
|
||||
class MockH1V2(MockHidapiDevice):
|
||||
def __init__(self, raw_speed_channels, raw_led_channels):
|
||||
super().__init__()
|
||||
self.raw_speed_channels = raw_speed_channels
|
||||
self.raw_led_channels = raw_led_channels
|
||||
|
||||
def write(self, data):
|
||||
reply = bytearray(64)
|
||||
if data[0:2] == [0x10, 0x01]:
|
||||
reply[0:2] = [0x11, 0x01]
|
||||
elif data[0:2] == [0x20, 0x03]:
|
||||
reply[0:2] = [0x21, 0x03]
|
||||
reply[14] = self.raw_led_channels
|
||||
if self.raw_led_channels > 1:
|
||||
reply[15 + 1 * 6] = 0x10
|
||||
reply[15 + 2 * 6] = 0x11
|
||||
self.preload_read(Report(reply[0], reply[1:]))
|
||||
return super().write(data)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_smart2():
|
||||
raw = MockH1V2(raw_speed_channels=2, raw_led_channels=0)
|
||||
dev = H1V2(raw, "Mock H1 V2", speed_channel_count=2, color_channel_count=0)
|
||||
dev.connect()
|
||||
return dev
|
||||
|
||||
|
||||
@pytest.mark.parametrize("has_hwmon,direct_access", [(False, False), (True, True), (True, False)])
|
||||
def test_initializes(mock_smart2, has_hwmon, direct_access, tmp_path):
|
||||
if has_hwmon:
|
||||
mock_smart2._hwmon = HwmonDevice("mock_module", tmp_path)
|
||||
|
||||
_ = mock_smart2.initialize(direct_access=direct_access)
|
||||
|
||||
writes = len(mock_smart2.device.sent)
|
||||
if not has_hwmon or direct_access:
|
||||
assert writes == 4
|
||||
else:
|
||||
assert writes == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("has_hwmon,direct_access", [(False, False), (False, True), (True, True)])
|
||||
def test_reads_status(mock_smart2, has_hwmon, direct_access):
|
||||
if has_hwmon:
|
||||
mock_smart2._hwmon = HwmonDevice(None, None)
|
||||
|
||||
mock_smart2.device.preload_read(Report(0, SAMPLE_STATUS))
|
||||
|
||||
expected = [
|
||||
("Fan 1 control mode", "PWM", ""),
|
||||
("Fan 1 duty", 30, "%"),
|
||||
("Fan 1 speed", 644, "rpm"),
|
||||
("Fan 2 control mode", "PWM", ""),
|
||||
("Fan 2 duty", 100, "%"),
|
||||
("Fan 2 speed", 1785, "rpm"),
|
||||
("Pump speed", 4000, "rpm"),
|
||||
]
|
||||
|
||||
got = mock_smart2.get_status(direct_access=direct_access)
|
||||
|
||||
assert sorted(got) == sorted(expected)
|
||||
|
||||
|
||||
def test_constructor_sets_up_all_channels(mock_smart2):
|
||||
assert mock_smart2._speed_channels == {
|
||||
"fan1": (0, 0, 100),
|
||||
"fan2": (1, 0, 100),
|
||||
}
|
||||
|
||||
|
||||
def test_not_totally_broken(mock_smart2):
|
||||
_ = mock_smart2.initialize()
|
||||
mock_smart2.device.preload_read(Report(0, [0x75, 0x02] + [0] * 62))
|
||||
_ = mock_smart2.get_status()
|
||||
mock_smart2.set_fixed_speed(channel="fan2", duty=50)
|
Loading…
Reference in New Issue