Support for NZXT H1 V2 case Smart Device (#451)

This commit is contained in:
Eugenio Rossi 2022-05-05 11:38:53 +02:00 committed by GitHub
parent 3e1d3a4115
commit b66268e388
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 159 additions and 7 deletions

View File

@ -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> |

View File

@ -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.*

View File

@ -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"

View File

@ -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

90
tests/test_nzxt_h1_v2.py Normal file
View File

@ -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)