liquidctl/tests/test_hydro_platinum.py

393 lines
14 KiB
Python

import pytest
from _testutils import MockHidapiDevice, Report, MockRuntimeStorage
from liquidctl.driver.hydro_platinum import HydroPlatinum, _sequence
from liquidctl.util import mkCrcFun
_SAMPLE_PATH = (r'IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/XHC@14/XH'
r'C@14000000/HS11@14a00000/USB2.0 Hub@14a00000/AppleUSB20InternalH'
r'ub@14a00000/AppleUSB20HubPort@14a10000/USB2.0 Hub@14a10000/Apple'
r'USB20Hub@14a10000/AppleUSB20HubPort@14a12000/H100i Platinum@14a1'
r'2000/IOUSBHostInterface@0/AppleUserUSBHostHIDDevice+Win\\#!&3142')
_WIN_MAX_PATH = 260 # Windows API should be the bottleneck
_crc8 = mkCrcFun('crc-8')
@pytest.fixture
def h115iPlatinumDevice():
description = 'Mock H115i Platinum'
kwargs = {'fan_count': 2, 'fan_leds': 4}
device = _MockHydroPlatinumDevice()
dev = HydroPlatinum(device, description, **kwargs)
runtime_storage = MockRuntimeStorage(key_prefixes=['testing'])
runtime_storage.store('leds_enabled', 0)
dev.connect(runtime_storage=runtime_storage)
return dev
@pytest.fixture
def h100iPlatinumSeDevice():
description = 'Mock H100i Platinum SE'
kwargs = {'fan_count': 2, 'fan_leds': 16}
device = _MockHydroPlatinumDevice()
dev = HydroPlatinum(device, description, **kwargs)
runtime_storage = MockRuntimeStorage(key_prefixes=['testing'])
runtime_storage.store('leds_enabled', 0)
dev.connect(runtime_storage=runtime_storage)
return dev
@pytest.fixture
def h150iProXTDevice():
description = 'Mock H150i Pro XT'
kwargs = {'fan_count': 3, 'fan_leds': 0}
device = _MockHydroPlatinumDevice()
dev = HydroPlatinum(device, description, **kwargs)
runtime_storage = MockRuntimeStorage(key_prefixes=['testing'])
runtime_storage.store('leds_enabled', 0)
dev.connect(runtime_storage=runtime_storage)
return dev
class _MockHydroPlatinumDevice(MockHidapiDevice):
def __init__(self):
super().__init__(vendor_id=0xffff, product_id=0x0c17, address=_SAMPLE_PATH)
self.fw_version = (1, 1, 15)
self.temperature = 30.9
self.fan1_speed = 1499
self.fan2_speed = 1512
self.fan3_speed = 1777
self.pump_speed = 2702
def read(self, length):
pre = super().read(length)
if pre:
return pre
buf = bytearray(64)
buf[2] = self.fw_version[0] << 4 | self.fw_version[1]
buf[3] = self.fw_version[2]
buf[7] = int((self.temperature - int(self.temperature)) * 255)
buf[8] = int(self.temperature)
buf[14] = round(.10 * 255)
buf[15:17] = self.fan1_speed.to_bytes(length=2, byteorder='little')
buf[21] = round(.20 * 255)
buf[22:24] = self.fan2_speed.to_bytes(length=2, byteorder='little')
buf[28] = round(.70 * 255)
buf[29:31] = self.pump_speed.to_bytes(length=2, byteorder='little')
buf[42] = round(.30 * 255)
buf[43:44] = self.fan3_speed.to_bytes(length=2, byteorder='little')
buf[-1] = _crc8(buf[1:-1])
return buf[:length]
def test_sequence_numbers_are_correctly_generated():
runtime_storage = MockRuntimeStorage(key_prefixes=['testing'])
sequence = _sequence(runtime_storage)
for i in range(1, 32):
assert next(sequence) == i
for i in range(1, 32):
assert next(sequence) == i
def test_h115i_platinum_device_connect(h115iPlatinumDevice):
dev = h115iPlatinumDevice
dev.disconnect() # the fixture had by default connected to the device
def mock_open():
nonlocal opened
opened = True
dev.device.open = mock_open
opened = False
with dev.connect() as cm:
assert cm == dev
assert opened
def test_h115i_platinum_device_command_format(h115iPlatinumDevice):
dev = h115iPlatinumDevice
dev.initialize()
dev.get_status()
dev.set_fixed_speed(channel='fan', duty=100)
dev.set_speed_profile(channel='fan', profile=[])
dev.set_color(channel='led', mode='off', colors=[])
assert len(dev.device.sent) == 9
for i, (report, data) in enumerate(dev.device.sent):
assert report == 0
assert len(data) == 64
assert data[0] == 0x3f
assert data[1] >> 3 == i + 1
assert data[-1] == _crc8(bytes(data[1:-1]))
def test_h115i_platinum_device_command_format_enabled(h115iPlatinumDevice):
dev = h115iPlatinumDevice
# test that the led enable messages are not sent if they are sent again
dev.initialize()
dev._data.store('leds_enabled', 1)
dev.get_status()
dev.set_fixed_speed(channel='fan', duty=100)
dev.set_speed_profile(channel='fan', profile=[])
dev.set_color(channel='led', mode='off', colors=[])
assert len(dev.device.sent) == 6
for i, (report, data) in enumerate(dev.device.sent):
assert report == 0
assert len(data) == 64
assert data[0] == 0x3f
assert data[1] >> 3 == i + 1
assert data[-1] == _crc8(bytes(data[1:-1]))
def test_h115i_platinum_device_get_status(h115iPlatinumDevice):
dev = h115iPlatinumDevice
temp, fan1, fan1d, fan2, fan2d, pump, pumpd = dev.get_status()
assert temp[1] == pytest.approx(dev.device.temperature, abs=1 / 255)
assert fan1[1] == dev.device.fan1_speed
assert fan1d[1] == 10
assert fan2[1] == dev.device.fan2_speed
assert fan2d[1] == 20
assert pump[1] == dev.device.pump_speed
assert pumpd[1] == 70
assert dev.device.sent[0].data[1] & 0b111 == 0
assert dev.device.sent[0].data[2] == 0xff
def test_h150i_pro_xt_device_get_status(h150iProXTDevice):
dev = h150iProXTDevice
temp, fan1, fan1d, fan2, fan2d, fan3, fan3d, pump, pumpd = dev.get_status()
assert temp[1] == pytest.approx(dev.device.temperature, abs=1 / 255)
assert fan1[1] == dev.device.fan1_speed
assert fan1d[1] == 10
assert fan2[1] == dev.device.fan2_speed
assert fan2d[1] == 20
assert fan3[1] == dev.device.fan3_speed
assert fan3d[1] == 30
assert pump[1] == dev.device.pump_speed
assert pumpd[1] == 70
assert dev.device.sent[0].data[1] & 0b111 == 0
assert dev.device.sent[0].data[2] == 0xff
def test_h115i_platinum_device_handle_real_statuses(h115iPlatinumDevice):
dev = h115iPlatinumDevice
samples = [
(
'ff08110f0001002c1e0000aee803aed10700aee803aece0701aa0000aa9c0900'
'0000000000000000000000000000000000000000000000000000000000000010'
),
(
'ff40110f009e14011b0102ffe8037e6a0502ffe8037e6d0501aa0000aa350901'
'0000000000000000000000000000000000000000000000000000000000000098'
)
]
for sample in samples:
dev.device.preload_read(Report(0, bytes.fromhex(sample)))
status = dev.get_status()
assert len(status) == 7
assert status[0][1] != dev.device.temperature
def test_h115i_platinum_device_initialize_status(h115iPlatinumDevice):
dev = h115iPlatinumDevice
dev._data.store('leds_enabled', 1)
(fw_version, ) = dev.initialize()
assert fw_version[1] == '%d.%d.%d' % dev.device.fw_version
assert dev._data.load('leds_enabled', of_type=int, default=1) == 0
def test_h115i_platinum_device_common_cooling_prefix(h115iPlatinumDevice):
dev = h115iPlatinumDevice
dev.initialize(pump_mode='extreme')
dev.set_fixed_speed(channel='fan', duty=42)
dev.set_speed_profile(channel='fan', profile=[(20, 0), (55, 100)])
assert len(dev.device.sent) == 3
for _, data in dev.device.sent:
assert data[0x1] & 0b111 == 0
assert data[0x2] == 0x14
# opaque but apparently important prefix (see @makk50's comments in #82):
assert data[0x3:0xb] == [0x0, 0xff, 0x5] + 5 * [0xff]
def test_h115i_platinum_device_set_pump_mode(h115iPlatinumDevice):
dev = h115iPlatinumDevice
dev.initialize(pump_mode='extreme')
assert dev.device.sent[0].data[0x17] == 0x2
with pytest.raises(KeyError):
dev.initialize(pump_mode='invalid')
def test_h115i_platinum_device_fixed_fan_speeds(h115iPlatinumDevice):
dev = h115iPlatinumDevice
dev.set_fixed_speed(channel='fan', duty=42)
dev.set_fixed_speed(channel='fan1', duty=84)
assert dev.device.sent[-1].data[0x0b] == 0x2
assert dev.device.sent[-1].data[0x10] / 2.55 == pytest.approx(84, abs=1 / 2.55)
assert dev.device.sent[-1].data[0x11] == 0x2
assert dev.device.sent[-1].data[0x16] / 2.55 == pytest.approx(42, abs=1 / 2.55)
with pytest.raises(ValueError):
dev.set_fixed_speed('invalid', 0)
def test_h150i_pro_xt_device_fixed_fan_speeds(h150iProXTDevice):
dev = h150iProXTDevice
dev.set_fixed_speed(channel='fan', duty=42)
dev.set_fixed_speed(channel='fan1', duty=84)
dev.set_fixed_speed(channel='fan3', duty=50)
assert dev.device.sent[-1].data[0x01] & 0x7 == 0b000
assert dev.device.sent[-2].data[0x02] == 0x14
assert dev.device.sent[-1].data[0x0b] == 0x2
assert dev.device.sent[-1].data[0x10] / 2.55 == pytest.approx(84, abs=1 / 2.55)
assert dev.device.sent[-1].data[0x11] == 0x2
assert dev.device.sent[-1].data[0x16] / 2.55 == pytest.approx(42, abs=1 / 2.55)
assert dev.device.sent[-2].data[0x01] & 0x7 == 0b011
assert dev.device.sent[-2].data[0x02] == 0x14
assert dev.device.sent[-2].data[0x0b] == 0x2
assert dev.device.sent[-2].data[0x10] / 2.55 == pytest.approx(50, abs=1 / 2.55)
assert dev.device.sent[-2].data[0x11] == 0x00
assert dev.device.sent[-2].data[0x16] == 0x00
def test_h115i_platinum_device_custom_fan_profiles(h115iPlatinumDevice):
dev = h115iPlatinumDevice
dev.set_speed_profile(channel='fan', profile=iter([(20, 0), (55, 100)]))
dev.set_speed_profile(channel='fan1', profile=iter([(30, 20), (50, 80)]))
assert dev.device.sent[-1].data[0x0b] == 0x0
assert dev.device.sent[-1].data[0x1d] == 7
assert dev.device.sent[-1].data[0x1e:0x2c] == [30, 51, 50, 204] + 5 * [60, 255]
assert dev.device.sent[-1].data[0x11] == 0x0
assert dev.device.sent[-1].data[0x2c:0x3a] == [20, 0, 55, 255] + 5 * [60, 255]
with pytest.raises(ValueError):
dev.set_speed_profile('invalid', [])
with pytest.raises(ValueError):
dev.set_speed_profile('fan', zip(range(10), range(10)))
def test_h115i_platinum_device_address_leds(h115iPlatinumDevice):
dev = h115iPlatinumDevice
colors = [[i + 3, i + 2, i + 1] for i in range(0, 24 * 3, 3)]
encoded = list(range(1, 24 * 3 + 1))
dev.set_color(channel='led', mode='super-fixed', colors=iter(colors))
assert len(dev.device.sent) == 5 # 3 for enable, 2 for off
assert dev.device.sent[0].data[1] & 0b111 == 0b001
assert dev.device.sent[1].data[1] & 0b111 == 0b010
assert dev.device.sent[2].data[1] & 0b111 == 0b011
assert dev.device.sent[3].data[1] & 0b111 == 0b100
assert dev.device.sent[3].data[2:62] == encoded[:60]
assert dev.device.sent[4].data[1] & 0b111 == 0b101
assert dev.device.sent[4].data[2:14] == encoded[60:]
def test_h100i_platinum_se_device_address_leds(h100iPlatinumSeDevice):
dev = h100iPlatinumSeDevice
colors = [[i + 3, i + 2, i + 1] for i in range(0, 48 * 3, 3)]
encoded = list(range(1, 48 * 3 + 1))
dev.set_color(channel='led', mode='super-fixed', colors=iter(colors))
assert len(dev.device.sent) == 6 # 3 for enable, 3 for the leds
assert dev.device.sent[0].data[1] & 0b111 == 0b001
assert dev.device.sent[1].data[1] & 0b111 == 0b010
assert dev.device.sent[2].data[1] & 0b111 == 0b011
assert dev.device.sent[3].data[1] & 0b111 == 0b100
assert dev.device.sent[3].data[2:62] == encoded[:60]
assert dev.device.sent[4].data[1] & 0b111 == 0b101
assert dev.device.sent[4].data[2:62] == encoded[60:120]
assert dev.device.sent[5].data[1] & 0b111 == 0b110
assert dev.device.sent[5].data[2:26] == encoded[120:]
def test_h150i_pro_xt_device_address_leds(h150iProXTDevice):
dev = h150iProXTDevice
colors = [[i + 3, i + 2, i + 1] for i in range(0, 16 * 3, 3)]
encoded = list(range(1, 16 * 3 + 1))
dev.set_color(channel='led', mode='super-fixed', colors=iter(colors))
assert len(dev.device.sent) == 4 # 3 for enable, 1 for the leds
assert dev.device.sent[0].data[1] & 0b111 == 0b001
assert dev.device.sent[1].data[1] & 0b111 == 0b010
assert dev.device.sent[2].data[1] & 0b111 == 0b011
assert dev.device.sent[3].data[1] & 0b111 == 0b100
assert dev.device.sent[3].data[2:50] == encoded[:48]
def test_h115i_platinum_device_synchronize(h115iPlatinumDevice):
dev = h115iPlatinumDevice
colors = [[3, 2, 1]]
encoded = [1, 2, 3] * 24
dev.set_color(channel='led', mode='fixed', colors=iter(colors))
assert len(dev.device.sent) == 5 # 3 for enable, 2 for off
assert dev.device.sent[0].data[1] & 0b111 == 0b001
assert dev.device.sent[1].data[1] & 0b111 == 0b010
assert dev.device.sent[2].data[1] & 0b111 == 0b011
assert dev.device.sent[3].data[1] & 0b111 == 0b100
assert dev.device.sent[3].data[2:62] == encoded[:60]
assert dev.device.sent[4].data[1] & 0b111 == 0b101
assert dev.device.sent[4].data[2:14] == encoded[60:]
def test_h115i_platinum_device_leds_off(h115iPlatinumDevice):
dev = h115iPlatinumDevice
dev.set_color(channel='led', mode='off', colors=iter([]))
assert len(dev.device.sent) == 5 # 3 for enable, 2 for off
for _, data in dev.device.sent[3:5]:
assert data[2:62] == [0] * 60
def test_h115i_platinum_device_invalid_color_modes(h115iPlatinumDevice):
dev = h115iPlatinumDevice
with pytest.raises(ValueError):
dev.set_color('led', 'invalid', [])
with pytest.raises(ValueError):
dev.set_color('invalid', 'off', [])
assert len(dev.device.sent) == 0
def test_h115i_platinum_device_short_enough_storage_path():
description = 'Mock H115i Platinum'
kwargs = {'fan_count': 2, 'fan_leds': 4}
device = _MockHydroPlatinumDevice()
dev = HydroPlatinum(device, description, **kwargs)
dev.connect()
assert len(dev._data._backend._write_dir) < _WIN_MAX_PATH
assert dev._data._backend._write_dir.endswith('3142')
def test_h115i_platinum_device_bad_stored_data(h115iPlatinumDevice):
h115iPlatinumDevice
# TODO
pass