Solaar/tests/logitech_receiver/test_hidpp20_complex.py

1283 lines
49 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## Copyright (C) 2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import pytest
import yaml
from logitech_receiver import common
from logitech_receiver import exceptions
from logitech_receiver import hidpp20
from logitech_receiver import hidpp20_constants
from logitech_receiver import special_keys
from logitech_receiver.device import CenturionReceiver
from logitech_receiver.hidpp20 import KeyFlag
from logitech_receiver.hidpp20 import MappingFlag
from logitech_receiver.hidpp20_constants import GestureId
from . import fake_hidpp
_hidpp20 = hidpp20.Hidpp20()
device_offline = fake_hidpp.Device("REGISTERS", False)
device_registers = fake_hidpp.Device("OFFLINE", True, 1.0)
device_nofeatures = fake_hidpp.Device("NOFEATURES", True, 4.5)
device_zerofeatures = fake_hidpp.Device("ZEROFEATURES", True, 4.5, [fake_hidpp.Response("0000", 0x0000, "0001")])
device_broken = fake_hidpp.Device(
"BROKEN", True, 4.5, [fake_hidpp.Response("0500", 0x0000, "0001"), fake_hidpp.Response(None, 0x0100)]
)
device_standard = fake_hidpp.Device("STANDARD", True, 4.5, fake_hidpp.r_keyboard_2)
@pytest.mark.parametrize(
"device, expected_result, expected_count",
[
(device_offline, False, 0),
(device_registers, False, 0),
(device_nofeatures, False, 0),
(device_zerofeatures, False, 0),
(device_broken, False, 0),
(device_standard, True, 9),
],
)
def test_FeaturesArray_check(device, expected_result, expected_count):
featuresarray = hidpp20.FeaturesArray(device)
result = featuresarray._check()
result2 = featuresarray._check()
assert result == expected_result
assert result2 == expected_result
assert (hidpp20_constants.SupportedFeature.ROOT in featuresarray) == expected_result
assert len(featuresarray) == expected_count
assert bool(featuresarray) == expected_result
@pytest.mark.parametrize(
"device, expected0, expected1, expected2, expected5, expected5v",
[
(device_zerofeatures, None, None, None, None, None),
(device_standard, 0x0000, 0x0001, 0x0020, hidpp20_constants.SupportedFeature.REPROG_CONTROLS_V4, 3),
],
)
def test_FeaturesArray_get_feature(device, expected0, expected1, expected2, expected5, expected5v):
featuresarray = hidpp20.FeaturesArray(device)
device.features = featuresarray
result0 = featuresarray.get_feature(0)
result1 = featuresarray.get_feature(1)
result2 = featuresarray.get_feature(2)
result5 = featuresarray.get_feature(5)
result2r = featuresarray.get_feature(2)
result5v = featuresarray.get_feature_version(hidpp20_constants.SupportedFeature.REPROG_CONTROLS_V4)
assert result0 == expected0
assert result1 == expected1
assert result2 == expected2
assert result2r == expected2
assert result5 == expected5
assert result5v == expected5v
@pytest.mark.parametrize(
"device, expected_result",
[
(device_zerofeatures, []),
(
device_standard,
[
(hidpp20_constants.SupportedFeature.ROOT, 0),
(hidpp20_constants.SupportedFeature.FEATURE_SET, 1),
(hidpp20_constants.SupportedFeature.CONFIG_CHANGE, 2),
(hidpp20_constants.SupportedFeature.DEVICE_FW_VERSION, 3),
(hidpp20_constants.SupportedFeature.CENTURION_DEVICE_INFO, 4),
(hidpp20_constants.SupportedFeature.REPROG_CONTROLS_V4, 5),
# Indices 6 and 7 have no responses — get_feature returns None, enumerate skips them
(hidpp20_constants.SupportedFeature.BATTERY_STATUS, 8),
],
),
],
)
def test_FeaturesArray_enumerate(device, expected_result):
featuresarray = hidpp20.FeaturesArray(device)
result = list(featuresarray.enumerate())
assert result == expected_result
def test_FeaturesArray_setitem():
featuresarray = hidpp20.FeaturesArray(device_standard)
featuresarray[hidpp20_constants.SupportedFeature.ROOT] = 3
featuresarray[hidpp20_constants.SupportedFeature.FEATURE_SET] = 5
featuresarray[hidpp20_constants.SupportedFeature.FEATURE_SET] = 4
assert featuresarray[hidpp20_constants.SupportedFeature.FEATURE_SET] == 4
assert featuresarray.inverse[4] == hidpp20_constants.SupportedFeature.FEATURE_SET
def test_FeaturesArray_delitem():
featuresarray = hidpp20.FeaturesArray(device_standard)
with pytest.raises(ValueError):
del featuresarray[5]
@pytest.mark.parametrize(
"device, expected0, expected1, expected2, expected1v",
[(device_zerofeatures, None, None, None, None), (device_standard, 0, 5, None, 3)],
)
def test_FeaturesArray_getitem(device, expected0, expected1, expected2, expected1v):
featuresarray = hidpp20.FeaturesArray(device)
device.features = featuresarray
result_get0 = featuresarray[hidpp20_constants.SupportedFeature.ROOT]
result_get1 = featuresarray[hidpp20_constants.SupportedFeature.REPROG_CONTROLS_V4]
result_get2 = featuresarray[hidpp20_constants.SupportedFeature.GKEY]
result_1v = featuresarray.get_feature_version(hidpp20_constants.SupportedFeature.REPROG_CONTROLS_V4)
assert result_get0 == expected0
assert result_get1 == expected1
assert result_get2 == expected2
assert result_1v == expected1v
@pytest.mark.parametrize(
"device, index, cid, task_id, flags, default_task, expected_flags",
[
(device_standard, 2, 1, 1, 0x30, "Volume Up", KeyFlag.REPROGRAMMABLE | KeyFlag.DIVERTABLE),
(device_standard, 1, 2, 2, 0x20, "Volume Down", KeyFlag.DIVERTABLE),
],
)
def test_reprogrammable_key_key(device, index, cid, task_id, flags, default_task, expected_flags):
key = hidpp20.ReprogrammableKey(device, index, cid, task_id, flags)
assert key._device == device
assert key.index == index
assert key._cid == cid
assert key._tid == task_id
assert key._flags == flags
assert key.key == special_keys.CONTROL[cid]
assert key.default_task == common.NamedInt(cid, default_task)
assert key.flags == expected_flags
@pytest.mark.parametrize(
"device, index, cid, task_id, flags, pos, group, gmask, default_task, expected_flags, group_names",
[
(
device_standard,
1,
0x51,
0x39,
0x60,
0,
1,
1,
"Right Click",
KeyFlag.DIVERTABLE | KeyFlag.PERSISTENTLY_DIVERTABLE,
["g1"],
),
(
device_standard,
2,
0x52,
0x3A,
0x11,
1,
2,
3,
"Mouse Middle Button",
KeyFlag.MSE | KeyFlag.REPROGRAMMABLE,
["g1", "g2"],
),
(
device_standard,
3,
0x53,
0x3C,
0x110,
2,
2,
7,
"Mouse Back Button",
KeyFlag.REPROGRAMMABLE | KeyFlag.RAW_XY,
["g1", "g2", "g3"],
),
],
)
def test_reprogrammable_key_v4_key(
device, index, cid, task_id, flags, pos, group, gmask, default_task, expected_flags, group_names
):
key = hidpp20.ReprogrammableKeyV4(device, index, cid, task_id, flags, pos, group, gmask)
assert key._device == device
assert key.index == index
assert key._cid == cid
assert key._tid == task_id
assert key._flags == flags
assert key.pos == pos
assert key.group == group
assert key._gmask == gmask
assert key.key == special_keys.CONTROL[cid]
assert key.default_task == common.NamedInt(cid, default_task)
assert key.flags == expected_flags
assert list(key.group_mask) == group_names
@pytest.mark.parametrize(
"responses, index, mapped_to, remappable_to, expected_mapping_flags",
[
(fake_hidpp.responses_key, 1, "Right Click", common.UnsortedNamedInts(Right_Click=81, Left_Click=80), MappingFlag(0)),
(fake_hidpp.responses_key, 2, "Left Click", None, MappingFlag.DIVERTED),
(fake_hidpp.responses_key, 3, "Mouse Back Button", None, MappingFlag.DIVERTED | MappingFlag.PERSISTENTLY_DIVERTED),
(fake_hidpp.responses_key, 4, "Mouse Forward Button", None, MappingFlag.DIVERTED | MappingFlag.RAW_XY_DIVERTED),
],
)
# these fields need access all the key data, so start by setting up a device and its key data
def test_reprogrammable_key_v4_query(responses, index, mapped_to, remappable_to, expected_mapping_flags):
device = fake_hidpp.Device(
"KEY", responses=responses, feature=hidpp20_constants.SupportedFeature.REPROG_CONTROLS_V4, offset=5
)
device._keys = _hidpp20.get_keys(device)
key = device.keys[index]
assert key.mapped_to == mapped_to
assert (key.remappable_to == remappable_to) or remappable_to is None
assert key.mapping_flags == expected_mapping_flags
@pytest.mark.parametrize(
"responses, index, diverted, persistently_diverted, rawXY_reporting, remap, sets",
[
(fake_hidpp.responses_key, 1, True, False, True, 0x52, ["0051080000"]),
(fake_hidpp.responses_key, 2, False, True, False, 0x51, ["0052020000", "0052200000", "0052000051"]),
(fake_hidpp.responses_key, 3, False, True, True, 0x50, ["0053020000", "00530C0000", "0053300000", "0053000050"]),
(fake_hidpp.responses_key, 4, False, False, False, 0x50, ["0056020000", "0056080000", "0056200000", "0056000050"]),
],
)
def test_reprogrammable_key_v4_set(responses, index, diverted, persistently_diverted, rawXY_reporting, remap, sets, mocker):
responses += [fake_hidpp.Response(r, 0x530, r) for r in sets]
device = fake_hidpp.Device(
"KEY", responses=responses, feature=hidpp20_constants.SupportedFeature.REPROG_CONTROLS_V4, offset=5
)
device._keys = _hidpp20.get_keys(device)
device._keys._ensure_all_keys_queried() # do this now so that the last requests are sets
spy_request = mocker.spy(device, "request")
key = device.keys[index]
_mapping_flags = key.mapping_flags
if hidpp20.KeyFlag.DIVERTABLE in key.flags or not diverted:
key.set_diverted(diverted)
else:
with pytest.raises(exceptions.FeatureNotSupported):
key.set_diverted(diverted)
assert (MappingFlag.DIVERTED in key.mapping_flags) == (diverted and hidpp20.KeyFlag.DIVERTABLE in key.flags)
if hidpp20.KeyFlag.PERSISTENTLY_DIVERTABLE in key.flags or not persistently_diverted:
key.set_persistently_diverted(persistently_diverted)
else:
with pytest.raises(exceptions.FeatureNotSupported):
key.set_persistently_diverted(persistently_diverted)
assert (hidpp20.MappingFlag.PERSISTENTLY_DIVERTED in key.mapping_flags) == (
persistently_diverted and hidpp20.KeyFlag.PERSISTENTLY_DIVERTABLE in key.flags
)
if hidpp20.KeyFlag.RAW_XY in key.flags or not rawXY_reporting:
key.set_rawXY_reporting(rawXY_reporting)
else:
with pytest.raises(exceptions.FeatureNotSupported):
key.set_rawXY_reporting(rawXY_reporting)
assert (MappingFlag.RAW_XY_DIVERTED in key.mapping_flags) == (rawXY_reporting and hidpp20.KeyFlag.RAW_XY in key.flags)
if remap in key.remappable_to or remap == 0:
key.remap(remap)
else:
with pytest.raises(exceptions.FeatureNotSupported):
key.remap(remap)
assert (key.mapped_to == remap) or (remap not in key.remappable_to and remap != 0)
fake_hidpp.match_requests(len(sets), responses, spy_request.call_args_list)
@pytest.mark.parametrize(
"r, index, cid, actionId, remapped, mask, status, action, modifiers, byts, remap",
[
(fake_hidpp.responses_key, 1, 0x0051, 0x02, 0x0002, 0x01, 0, "Mouse Button: 2", "Cntrl+", "02000201", "01000400"),
(fake_hidpp.responses_key, 2, 0x0052, 0x01, 0x0001, 0x00, 1, "Key: 1", "", "01000100", "02005004"),
(fake_hidpp.responses_key, 3, 0x0053, 0x02, 0x0001, 0x00, 1, "Mouse Button: 1", "", "02000100", "7FFFFFFF"),
],
)
def test_remappable_action(r, index, cid, actionId, remapped, mask, status, action, modifiers, byts, remap, mocker):
if int(remap, 16) == special_keys.KEYS_Default:
responses = r + [
fake_hidpp.Response("040000", 0x0000, "1C00"),
fake_hidpp.Response("00", 0x450, f"{cid:04X}" + "FF"),
]
else:
responses = r + [
fake_hidpp.Response("040000", 0x0000, "1C00"),
fake_hidpp.Response("00", 0x440, f"{cid:04X}" + "FF" + remap),
]
device = fake_hidpp.Device(
"KEY", responses=responses, feature=hidpp20_constants.SupportedFeature.REPROG_CONTROLS_V4, offset=5
)
key = hidpp20.PersistentRemappableAction(device, index, cid, actionId, remapped, mask, status)
spy_request = mocker.spy(device, "request")
assert key._device == device
assert key.index == index
assert key._cid == cid
assert key.actionId == actionId
assert key.remapped == remapped
assert key._modifierMask == mask
assert key.cidStatus == status
assert key.key == special_keys.CONTROL[cid]
assert key.actionType == special_keys.ACTIONID[actionId]
assert key.action == action
assert key.modifiers == modifiers
assert key.data_bytes.hex().upper() == byts
key.remap(bytes.fromhex(remap))
assert key.data_bytes.hex().upper() == (byts if int(remap, 16) == special_keys.KEYS_Default else remap)
if int(remap, 16) != special_keys.KEYS_Default:
fake_hidpp.match_requests(1, responses, spy_request.call_args_list)
# KeysArray methods tested in KeysArrayV4
# KeysArrayV2 not tested as there is no documentation
@pytest.mark.parametrize(
"device, index", [(device_zerofeatures, -1), (device_zerofeatures, 5), (device_standard, -1), (device_standard, 6)]
)
def test_KeysArrayV4_index_error(device, index):
keysarray = hidpp20.KeysArrayV4(device, 5)
with pytest.raises(IndexError):
keysarray[index]
with pytest.raises(IndexError):
keysarray._query_key(index)
@pytest.mark.parametrize("device, index, top, cid", [(device_standard, 0, 2, 0x0011), (device_standard, 4, 5, 0x0003)])
def test_KeysArrayV4_query_key(device, index, top, cid):
keysarray = hidpp20.KeysArrayV4(device, 5)
keysarray._query_key(index)
assert keysarray.keys[index]._cid == cid
assert len(keysarray[index:top]) == top - index
assert len(list(keysarray)) == 5
@pytest.mark.parametrize(
"device, count, index, cid, task_id, flags, pos, group, gmask",
[
(device_standard, 4, 0, 0x0011, 0x0012, 0xCDAB, 1, 2, 3),
(device_standard, 6, 1, 0x0111, 0x0022, 0xCDAB, 1, 2, 3),
(device_standard, 8, 3, 0x0311, 0x0032, 0xCDAB, 1, 2, 4),
],
)
def test_KeysArrayV4__getitem(device, count, index, cid, task_id, flags, pos, group, gmask):
keysarray = hidpp20.KeysArrayV4(device, count)
result = keysarray[index]
assert result._device == device
assert result.index == index
assert result._cid == cid
assert result._tid == task_id
assert result._flags == flags
assert result.pos == pos
assert result.group == group
assert result._gmask == gmask
@pytest.mark.parametrize(
"key, index", [(special_keys.CONTROL.Volume_Up_old, 2), (special_keys.CONTROL.Mute, 4), (special_keys.CONTROL.Next, None)]
)
def test_KeysArrayV4_index(key, index):
keysarray = hidpp20.KeysArrayV4(device_standard, 7)
result = keysarray.index(key)
assert result == index
device_key = fake_hidpp.Device(
"KEY", responses=fake_hidpp.responses_key, feature=hidpp20_constants.SupportedFeature.REPROG_CONTROLS_V4, offset=5
)
@pytest.mark.parametrize(
"key, expected_index, expected_mapped_to, expected_remappable_to",
[
(
special_keys.CONTROL.Left_Button,
0,
common.NamedInt(0x50, "Left Click"),
[common.NamedInt(0x50, "Left Click"), common.NamedInt(0x51, "Right Click")],
),
(
special_keys.CONTROL.Right_Button,
1,
common.NamedInt(0x51, "Right Click"),
[common.NamedInt(0x51, "Right Click"), common.NamedInt(0x50, "Left Click")],
),
(special_keys.CONTROL.Middle_Button, 2, common.NamedInt(0x50, "Left Click"), None),
(special_keys.CONTROL.Back_Button, 3, common.NamedInt(0x53, "Mouse Back Button"), None),
(special_keys.CONTROL.Forward_Button, 4, common.NamedInt(0x56, "Mouse Forward Button"), None),
(special_keys.CONTROL.Mouse_Gesture_Button, 5, common.NamedInt(0xC3, "Gesture Button Navigation"), None),
(special_keys.CONTROL.Smart_Shift, 6, common.NamedInt(0x50, "Left Click"), None),
(special_keys.CONTROL.Virtual_Gesture_Button, 7, common.NamedInt(0x51, "Right Click"), None),
],
)
def test_keys_array_v4_key(key, expected_index, expected_mapped_to, expected_remappable_to):
device_key._keys = _hidpp20.get_keys(device_key)
device_key._keys._ensure_all_keys_queried()
index = device_key._keys.index(key)
mapped_to = device_key._keys[expected_index].mapped_to
remappable_to = device_key._keys[expected_index].remappable_to
assert index == expected_index
assert mapped_to == expected_mapped_to
if expected_remappable_to is not None:
assert list(remappable_to) == expected_remappable_to
@pytest.mark.parametrize(
"device, index", [(device_zerofeatures, -1), (device_zerofeatures, 5), (device_standard, -1), (device_standard, 6)]
)
def test_KeysArrayPersistent_index_error(device, index):
keysarray = hidpp20.KeysArrayPersistent(device, 5)
with pytest.raises(IndexError):
keysarray[index]
with pytest.raises(IndexError):
keysarray._query_key(index)
@pytest.mark.parametrize(
"responses, key, index, mapped_to, capabilities",
[
(fake_hidpp.responses_remap, special_keys.CONTROL.Left_Button, 0, common.NamedInt(0x01, "Mouse Button Left"), 0x41),
(fake_hidpp.responses_remap, special_keys.CONTROL.Right_Button, 1, common.NamedInt(0x01, "Mouse Button Left"), 0x41),
(fake_hidpp.responses_remap, special_keys.CONTROL.Middle_Button, 2, common.NamedInt(0x51, "DOWN"), 0x41),
],
)
def test_KeysArrayPersistent_key(responses, key, index, mapped_to, capabilities):
device = fake_hidpp.Device(
"REMAP", responses=responses, feature=hidpp20_constants.SupportedFeature.PERSISTENT_REMAPPABLE_ACTION
)
device._remap_keys = _hidpp20.get_remap_keys(device)
device._remap_keys._ensure_all_keys_queried()
assert device._remap_keys.index(key) == index
assert device._remap_keys[index].remapped == mapped_to
assert device._remap_keys.capabilities == capabilities
@pytest.mark.parametrize(
"id, length, minimum, maximum, widget, min, max, wid, string",
[
("left", 1, 5, 8, "Widget", 5, 8, "Widget", "left"),
("left", 1, None, None, None, 0, 255, "Scale", "left"),
],
)
def test_SubParam(id, length, minimum, maximum, widget, min, max, wid, string):
subparam = hidpp20.SubParam(id, length, minimum, maximum, widget)
assert subparam.id == id
assert subparam.length == length
assert subparam.minimum == min
assert subparam.maximum == max
assert subparam.widget == wid
assert subparam.__str__() == string
assert subparam.__repr__() == string
@pytest.mark.parametrize(
"device, low, high, next_index, next_diversion_index, name, cbe, si, sdi, eom, dom",
[
(device_standard, 0x01, 0x01, 5, 10, GestureId.TAP_1_FINGER, True, 5, None, (0, 0x20), (None, None)),
(device_standard, 0x03, 0x02, 6, 11, GestureId.TAP_3_FINGER, False, None, 11, (None, None), (1, 0x08)),
],
)
def test_gesture(device, low, high, next_index, next_diversion_index, name, cbe, si, sdi, eom, dom):
gesture = hidpp20.Gesture(device, low, high, next_index, next_diversion_index)
assert gesture._device == device
assert gesture.id == low
assert gesture.gesture == name
assert gesture.can_be_enabled == cbe
assert gesture.can_be_enabled == cbe
assert gesture.index == si
assert gesture.diversion_index == sdi
assert gesture.enable_offset_mask() == eom
assert gesture.diversion_offset_mask() == dom
assert gesture.as_int() == low
assert int(gesture) == low
@pytest.mark.parametrize(
"responses, gest, enabled, diverted, set_result, unset_result, divert_result, undivert_result",
[
(fake_hidpp.responses_gestures, 20, None, None, None, None, None, None),
(fake_hidpp.responses_gestures, 1, True, False, "01", "00", "01", "00"),
(fake_hidpp.responses_gestures, 45, False, None, "01", "00", None, None),
],
)
def test_Gesture_set(responses, gest, enabled, diverted, set_result, unset_result, divert_result, undivert_result):
device = fake_hidpp.Device("GESTURE", responses=responses, feature=hidpp20_constants.SupportedFeature.GESTURE_2)
gestures = _hidpp20.get_gestures(device)
gesture = gestures.gesture(gest)
assert gesture.enabled() == enabled
assert gesture.diverted() == diverted
assert gesture.set(True) == (bytes.fromhex(set_result) if set_result is not None else None)
assert gesture.set(False) == (bytes.fromhex(unset_result) if unset_result is not None else None)
assert gesture.divert(True) == (bytes.fromhex(divert_result) if divert_result is not None else None)
assert gesture.divert(False) == (bytes.fromhex(undivert_result) if undivert_result is not None else None)
@pytest.mark.parametrize(
"responses, prm, id, index, size, value, default_value, write1, write2",
[
(fake_hidpp.responses_gestures, 4, hidpp20_constants.ParamId.SCALE_FACTOR, 0, 2, 256, 256, "0080", "0180"),
],
)
def test_param(responses, prm, id, index, size, value, default_value, write1, write2):
device = fake_hidpp.Device("GESTURE", responses=responses, feature=hidpp20_constants.SupportedFeature.GESTURE_2)
gestures = _hidpp20.get_gestures(device)
param = gestures.param(prm)
assert param.id == id
assert param.index == index
assert param.size == size
assert param.value == value
assert param.default_value == default_value
assert param.param == id
assert int(param) == id
assert param.write(bytes.fromhex(write1)).hex().upper() == f"{index:02X}" + write1 + "FF"
assert param.write(bytes.fromhex(write2)).hex().upper() == f"{index:02X}" + write2 + "FF"
@pytest.mark.parametrize(
"responses, id, s, byte_count, expected_value, expected_string",
[
(fake_hidpp.responses_gestures, 1, hidpp20.SpecGesture.DVI_FIELD_WIDTH, 1, 8, "[dvi field width=8]"),
(fake_hidpp.responses_gestures, 2, hidpp20.SpecGesture.FIELD_WIDTHS, 1, 8, "[field widths=8]"),
(fake_hidpp.responses_gestures, 3, hidpp20.SpecGesture.PERIOD_UNIT, 2, 2048, "[period unit=2048]"),
],
)
def test_spec(responses, id, s, byte_count, expected_value, expected_string):
device = fake_hidpp.Device("GESTURE", responses=responses, feature=hidpp20_constants.SupportedFeature.GESTURE_2)
gestures = _hidpp20.get_gestures(device)
spec = gestures.specs[id]
assert spec.id == id
assert spec.spec == s
assert spec.byte_count == byte_count
assert spec.value == expected_value
assert repr(spec) == expected_string
def test_Gestures():
device = fake_hidpp.Device(
"GESTURES", responses=fake_hidpp.responses_gestures, feature=hidpp20_constants.SupportedFeature.GESTURE_2
)
gestures = _hidpp20.get_gestures(device)
assert gestures
assert len(gestures.gestures) == 17
assert gestures.gesture(20) == gestures.gestures[20]
assert gestures.gesture_enabled(20) is None
assert gestures.gesture_enabled(1) is True
assert gestures.gesture_enabled(45) is False
assert gestures.enable_gesture(20) is None
assert gestures.enable_gesture(45) == bytes.fromhex("01")
assert gestures.disable_gesture(20) is None
assert gestures.disable_gesture(45) == bytes.fromhex("00")
assert len(gestures.params) == 1
assert gestures.param(4) == gestures.params[4]
assert gestures.get_param(4) == 256
assert gestures.set_param(4, 128) is None
assert len(gestures.specs) == 5
assert gestures.specs[2].value == 8
assert gestures.specs[4].value == 4
responses_backlight = [
fake_hidpp.Response("010118000001020003000400", 0x0400),
fake_hidpp.Response("0101FF00020003000400", 0x0410, "0101FF00020003000400"),
]
device_backlight = fake_hidpp.Device(
"BACKLIGHT", responses=responses_backlight, feature=hidpp20_constants.SupportedFeature.BACKLIGHT2
)
def test_Backlight():
backlight = _hidpp20.get_backlight(device_backlight)
result = backlight.write()
assert backlight
assert backlight.auto_supported
assert backlight.temp_supported
assert not backlight.perm_supported
assert backlight.dho == 0x0002
assert backlight.dhi == 0x0003
assert backlight.dpow == 0x0004
assert result is not None
@pytest.mark.parametrize(
"hex, ID, color, speed, period, intensity, ramp, form",
[
("FFFFFFFFFFFFFFFFFFFFFF", None, None, None, None, None, None, None),
("0000000000000000000000", common.NamedInt(0x0, "Disabled"), None, None, None, None, None, None),
("0120304010000000000000", common.NamedInt(0x1, "Static"), 0x203040, None, None, None, 0x10, None),
("0220304010000000000000", common.NamedInt(0x2, "Pulse"), 0x203040, 0x10, None, None, None, None),
("0800000000000000000000", common.NamedInt(0x8, "Boot"), None, None, None, None, None, None),
("0300000000005000000000", common.NamedInt(0x3, "Cycle"), None, None, 0x5000, 0x00, None, None),
("0A20304010005020000000", common.NamedInt(0xA, "Breathe"), 0x203040, None, 0x1000, 0x20, None, 0x50),
("0B20304000100000000000", common.NamedInt(0xB, "Ripple"), 0x203040, None, 0x1000, None, None, None),
("0A01020300500407000000", common.NamedInt(0xA, "Breathe"), 0x010203, None, 0x0050, 0x07, None, 0x04),
],
)
def test_LEDEffectSetting(hex, ID, color, speed, period, intensity, ramp, form):
byt = bytes.fromhex(hex)
setting = hidpp20.LEDEffectSetting.from_bytes(byt)
assert setting.ID == ID
if ID is None:
assert setting.bytes == byt
else:
assert getattr(setting, "color", None) == color
assert getattr(setting, "speed", None) == speed
assert getattr(setting, "period", None) == period
assert getattr(setting, "intensity", None) == intensity
assert getattr(setting, "ramp", None) == ramp
assert getattr(setting, "form", None) == form
assert setting.to_bytes() == byt
assert yaml.safe_load(yaml.dump(setting)) == setting
assert yaml.safe_load(str(setting)) == setting
@pytest.mark.parametrize(
"feature, function, response, ID, capabilities, period",
[
[
hidpp20_constants.SupportedFeature.COLOR_LED_EFFECTS,
0x20,
fake_hidpp.Response("0102000300040005", 0x0420, "010200"),
3,
4,
5,
],
[
hidpp20_constants.SupportedFeature.COLOR_LED_EFFECTS,
0x20,
fake_hidpp.Response("0102000700080009", 0x0420, "010200"),
7,
8,
9,
],
],
)
def test_LEDEffectInfo(feature, function, response, ID, capabilities, period):
device = fake_hidpp.Device(feature=feature, responses=[response])
info = hidpp20.LEDEffectInfo(feature, function, device, 1, 2)
assert info.zindex == 1
assert info.index == 2
assert info.ID == ID
assert info.capabilities == capabilities
assert info.period == period
@pytest.mark.parametrize(
"feature, function, offset, effect_function, responses, index, location, count, id_1",
[
[hidpp20_constants.SupportedFeature.COLOR_LED_EFFECTS, 0x10, 0, 0x20, fake_hidpp.zone_responses_1, 0, 1, 2, 0xB],
[hidpp20_constants.SupportedFeature.RGB_EFFECTS, 0x00, 1, 0x00, fake_hidpp.zone_responses_2, 0, 1, 2, 2],
],
)
def test_LEDZoneInfo(feature, function, offset, effect_function, responses, index, location, count, id_1):
device = fake_hidpp.Device(feature=feature, responses=responses, offset=0x07)
zone = hidpp20.LEDZoneInfo(feature, function, offset, effect_function, device, index)
assert zone.index == index
assert zone.location == location
assert zone.count == count
assert len(zone.effects) == count
assert zone.effects[1].ID == id_1
@pytest.mark.parametrize(
"responses, setting, expected_command",
[
[fake_hidpp.zone_responses_1, hidpp20.LEDEffectSetting(ID=0), None],
[fake_hidpp.zone_responses_1, hidpp20.LEDEffectSetting(ID=3, period=0x20, intensity=0x50), "000000000000000020500000"],
[
fake_hidpp.zone_responses_1,
hidpp20.LEDEffectSetting(ID=0xB, color=0x808080, period=0x20),
"000180808000002000000000",
],
],
)
def test_LEDZoneInfo_to_command(responses, setting, expected_command):
device = fake_hidpp.Device(feature=hidpp20_constants.SupportedFeature.COLOR_LED_EFFECTS, responses=responses, offset=0x07)
zone = hidpp20.LEDZoneInfo(hidpp20_constants.SupportedFeature.COLOR_LED_EFFECTS, 0x10, 0, 0x20, device, 0)
command = zone.to_command(setting)
assert command == (bytes.fromhex(expected_command) if expected_command is not None else None)
@pytest.mark.parametrize(
"feature, cls, responses, readable, count, count_0",
[
[
hidpp20_constants.SupportedFeature.COLOR_LED_EFFECTS,
hidpp20.LEDEffectsInfo,
fake_hidpp.effects_responses_1,
1,
1,
2,
],
[hidpp20_constants.SupportedFeature.RGB_EFFECTS, hidpp20.RGBEffectsInfo, fake_hidpp.effects_responses_2, 1, 1, 2],
],
)
def test_LED_RGB_EffectsInfo(feature, cls, responses, readable, count, count_0):
device = fake_hidpp.Device(feature=feature, responses=responses, offset=0x07)
effects = cls(device)
assert effects.readable == readable
assert effects.count == count
assert effects.zones[0].count == count_0
@pytest.mark.parametrize(
"hex, expected_behavior, sector, address, typ, val, modifiers, data, byt",
[
("05010203", 0x0, 0x501, 0x0203, None, None, None, None, None),
("15020304", 0x1, 0x502, 0x0304, None, None, None, None, None),
("8000FFFF", 0x8, None, None, 0x00, None, None, None, None),
("80010102", 0x8, None, None, 0x01, 0x0102, None, None, None),
("80020454", 0x8, None, None, 0x02, 0x54, 0x04, None, None),
("80030454", 0x8, None, None, 0x03, 0x0454, None, None, None),
("900AFF01", 0x9, None, None, None, 0x0A, None, 0x01, None),
("709090A0", 0x7, None, None, None, None, None, None, b"\x70\x90\x90\xa0"),
],
)
def test_button_bytes(hex, expected_behavior, sector, address, typ, val, modifiers, data, byt):
button = hidpp20.Button.from_bytes(bytes.fromhex(hex))
assert getattr(button, "behavior", None) == expected_behavior
assert getattr(button, "sector", None) == sector
assert getattr(button, "address", None) == address
assert getattr(button, "type", None) == typ
assert getattr(button, "value", None) == val
assert getattr(button, "modifiers", None) == modifiers
assert getattr(button, "data", None) == data
assert getattr(button, "bytes", None) == byt
assert button.to_bytes().hex().upper() == hex
assert yaml.safe_load(yaml.dump(button)).to_bytes().hex().upper() == hex
hex1 = (
"01010290018003000700140028FFFFFF"
"FFFF0000000000000000000000000000"
"8000FFFF900AFF00800204548000FFFF"
"900AFF00800204548000FFFF900AFF00"
"800204548000FFFF900AFF0080020454"
"8000FFFF900AFF00800204548000FFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"54004500370000000000000000000000"
"00000000000000000000000000000000"
"00000000000000000000000000000000"
"0A01020300500407000000FFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFF7C81"
)
hex2 = (
"01010290018003000700140028FFFFFF"
"FFFF0000000000000000000000000000"
"8000FFFF900AFF00800204548000FFFF"
"900AFF00800204548000FFFF900AFF00"
"800204548000FFFF900AFF0080020454"
"8000FFFF900AFF00800204548000FFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"0A01020300500407000000FFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFF27C9"
)
hex3 = (
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
"FFFFFFFFFFFFFFFFFFFFFFFFFF2307"
)
@pytest.mark.parametrize(
"hex, name, sector, enabled, buttons, gbuttons, resolutions, button, lighting",
[
(hex1, "TE7", 2, 1, 16, 0, [0x0190, 0x0380, 0x0700, 0x1400, 0x2800], "8000FFFF", "0A01020300500407000000"),
(hex2, "", 2, 1, 16, 0, [0x0190, 0x0380, 0x0700, 0x1400, 0x2800], "8000FFFF", "0A01020300500407000000"),
(hex3, "", 2, 1, 16, 0, [0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF], "FFFFFFFF", "FFFFFFFFFFFFFFFFFFFFFF"),
],
)
def test_onboard_profile_bytes(hex, name, sector, enabled, buttons, gbuttons, resolutions, button, lighting):
profile = hidpp20.OnboardProfile.from_bytes(sector, enabled, buttons, gbuttons, bytes.fromhex(hex))
assert profile.name == name
assert profile.sector == sector
assert profile.resolutions == resolutions
assert profile.buttons[0].to_bytes().hex().upper() == button
assert profile.lighting[0].to_bytes().hex().upper() == lighting
assert profile.to_bytes(len(hex) // 2).hex().upper() == hex
assert yaml.safe_load(yaml.dump(profile)).to_bytes(len(hex) // 2).hex().upper() == hex
@pytest.mark.parametrize(
"responses, name, count, buttons, gbuttons, sectors, size",
[
(fake_hidpp.responses_profiles, "ONB", 1, 2, 2, 1, 254),
(fake_hidpp.responses_profiles_rom, "ONB", 1, 2, 2, 1, 254),
(fake_hidpp.responses_profiles_rom_2, "ONB", 1, 2, 2, 1, 254),
],
)
def test_onboard_profiles_device(responses, name, count, buttons, gbuttons, sectors, size):
device = fake_hidpp.Device(
name, True, 4.5, responses=responses, feature=hidpp20_constants.SupportedFeature.ONBOARD_PROFILES, offset=0x9
)
device._profiles = None
profiles = _hidpp20.get_profiles(device)
assert profiles
assert profiles.version == hidpp20.OnboardProfilesVersion
assert profiles.name == name
assert profiles.count == count
assert profiles.buttons == buttons
assert profiles.gbuttons == gbuttons
assert profiles.sectors == sectors
assert profiles.size == size
assert len(profiles.profiles) == count
yml_dump = yaml.dump(profiles)
assert yaml.safe_load(yml_dump).to_bytes().hex() == profiles.to_bytes().hex()
# --- Centurion (PRO X 2 LIGHTSPEED headset) tests ---
device_centurion = fake_hidpp.Device("CENTURION", True, 2.6, fake_hidpp.r_centurion_headset, centurion=True)
def test_centurion_parent_feature_discovery():
"""Parent feature enumeration discovers CentPPBridge at index 3 and stores bridge index."""
dev = fake_hidpp.Device("CENT_PARENT", True, 2.6, fake_hidpp.r_centurion_headset, centurion=True)
featuresarray = hidpp20.FeaturesArray(dev)
dev.features = featuresarray
result = featuresarray._check()
assert result is True
assert featuresarray.count == 5
# Parent features registered
assert featuresarray[hidpp20_constants.SupportedFeature.ROOT] == 0
assert featuresarray[hidpp20_constants.SupportedFeature.FEATURE_SET] == 1
assert featuresarray[hidpp20_constants.SupportedFeature.CENTURION_DEVICE_INFO] == 2
assert featuresarray[hidpp20_constants.SupportedFeature.CENTURION_GENERIC_DFU] == 4
# Feature 0x0003 = CentPPBridge on Centurion (stored as DEVICE_FW_VERSION since same ID)
assert featuresarray[hidpp20_constants.SupportedFeature.DEVICE_FW_VERSION] == 3
# Bridge index stored on device
assert dev._centurion_bridge_index == 3
assert hasattr(dev, "_centurion_sub_features")
def test_centurion_sub_device_feature_discovery():
"""Sub-device feature discovery routes through bridge and populates _centurion_sub_features."""
dev = fake_hidpp.Device("CENT_SUB", True, 2.6, fake_hidpp.r_centurion_headset, centurion=True)
# Set up bridge responses for sub-device discovery:
# 1. CenturionRoot.GetFeature(0x0001) -> FeatureSet at sub-index 1
# 2. CenturionFeatureSet.GetFeatureId(index=0) -> bulk feature list
dev._bridge_responses = {
# CenturionRoot(idx=0).GetFeature(func=0) with feature_id=0x0001 -> sub_fs_index=1
(0x00, 0x00, "0001"): bytes([0x01, 0x00, 0x00]),
# CenturionFeatureSet(idx=1).GetFeatureId(func=0x10, start=0) -> 3 features
# Response: [count, (feat_hi, feat_lo, type, flags) × count]
(0x01, 0x10, "00"): bytes(
[
0x03, # 3 features
0x06,
0x04,
0x00,
0x00, # HEADSET_AUDIO_SIDETONE (0x0604) at sub-idx 0
0x06,
0x01,
0x00,
0x00, # HEADSET_MIC_MUTE (0x0601) at sub-idx 1
0x06,
0x11,
0x00,
0x00, # HEADSET_MIC_GAIN (0x0611) at sub-idx 2
]
),
}
featuresarray = hidpp20.FeaturesArray(dev)
dev.features = featuresarray
featuresarray._check()
# Sub-device features should be discovered
assert hidpp20_constants.SupportedFeature.HEADSET_AUDIO_SIDETONE in dev._centurion_sub_features
assert hidpp20_constants.SupportedFeature.HEADSET_MIC_MUTE in dev._centurion_sub_features
assert hidpp20_constants.SupportedFeature.HEADSET_MIC_GAIN in dev._centurion_sub_features
# Sub-device features should be in features array with their sub-device indices
assert featuresarray[hidpp20_constants.SupportedFeature.HEADSET_AUDIO_SIDETONE] == 0
assert featuresarray[hidpp20_constants.SupportedFeature.HEADSET_MIC_MUTE] == 1
assert featuresarray[hidpp20_constants.SupportedFeature.HEADSET_MIC_GAIN] == 2
# _centurion_sub_indices should map ALL sub-device features to their sub-device indices
assert dev._centurion_sub_indices[hidpp20_constants.SupportedFeature.HEADSET_AUDIO_SIDETONE] == 0
assert dev._centurion_sub_indices[hidpp20_constants.SupportedFeature.HEADSET_MIC_MUTE] == 1
assert dev._centurion_sub_indices[hidpp20_constants.SupportedFeature.HEADSET_MIC_GAIN] == 2
def test_centurion_feature_request_routes_sub_device():
"""feature_request() routes sub-device features through centurion_bridge_request()."""
dev = fake_hidpp.Device("CENT_ROUTE", True, 2.6, fake_hidpp.r_centurion_headset, centurion=True)
# Manually set up sub-device state (simulating completed discovery)
dev.features.count = 5 # mark discovery as complete so _check() short-circuits
dev._centurion_bridge_index = 3
dev._centurion_sub_features = {hidpp20_constants.SupportedFeature.HEADSET_AUDIO_SIDETONE}
dev.features[hidpp20_constants.SupportedFeature.HEADSET_AUDIO_SIDETONE] = 7 # sub-device index
# Set up bridge response for GetSidetoneLevel
dev._bridge_responses = {
(7, 0x00, ""): bytes([0x01, 0x00, 0x32]), # mic_id=1, mute=0, level=50
}
result = dev.feature_request(hidpp20_constants.SupportedFeature.HEADSET_AUDIO_SIDETONE, 0x00)
assert result is not None
assert result == bytes([0x01, 0x00, 0x32])
def test_centurion_feature_request_parent_not_routed():
"""feature_request() does NOT route parent features through bridge."""
dev = fake_hidpp.Device("CENT_PARENT2", True, 2.6, fake_hidpp.r_centurion_headset, centurion=True)
dev._centurion_bridge_index = 3
dev._centurion_sub_features = {hidpp20_constants.SupportedFeature.HEADSET_AUDIO_SIDETONE}
# FeatureSet is a parent feature — should go through normal request(), not bridge
featuresarray = hidpp20.FeaturesArray(dev)
dev.features = featuresarray
featuresarray._check()
# CENTURION_DEVICE_INFO is a parent feature at index 2 — requesting it should
# NOT go through bridge, it should go through the normal hidpp20.feature_request path
assert hidpp20_constants.SupportedFeature.CENTURION_DEVICE_INFO not in dev._centurion_sub_features
def test_centurion_bridge_request_write():
"""centurion_bridge_request with no_reply=True returns None immediately."""
dev = fake_hidpp.Device("CENT_WRITE", True, 2.6, fake_hidpp.r_centurion_headset, centurion=True)
dev._centurion_bridge_index = 3
dev._centurion_sub_features = {hidpp20_constants.SupportedFeature.HEADSET_AUDIO_SIDETONE}
dev.features[hidpp20_constants.SupportedFeature.HEADSET_AUDIO_SIDETONE] = 7
dev._bridge_responses = {} # no responses needed for no_reply
result = dev.centurion_bridge_request(7, 0x10, 0x32, no_reply=True)
assert result is None
def test_centurion_firmware_dedup():
"""get_firmware_centurion() deduplicates identical firmware entries."""
# Simulate parent device that returns the same firmware for every entity index
fw_response = "00" + "00" + "0105" + "04" + "44303031" + "00" * 20 # type=0, ver=1.05, name="D001"
responses = fake_hidpp.r_centurion_headset + [fake_hidpp.Response(fw_response, 0x0210, f"{i:02X}") for i in range(8)]
dev = fake_hidpp.Device("CENT_DEDUP", True, 2.6, responses, centurion=True)
fw = _hidpp20.get_firmware_centurion(dev)
# Should only get 1 entry, not 8
assert fw is not None
assert len(fw) == 1
assert fw[0].name == "D001"
def test_centurion_sub_device_firmware():
"""get_firmware_centurion_sub() queries sub-device firmware via bridge."""
dev = fake_hidpp.Device("CENT_SUBFW", True, 2.6, fake_hidpp.r_centurion_headset, centurion=True)
dev._centurion_bridge_index = 3
dev._centurion_sub_features = set()
dev._centurion_sub_indices = {hidpp20_constants.SupportedFeature.CENTURION_DEVICE_INFO: 2}
# Sub-device firmware: type=0 (firmware), ver=3.02, name="H001"
dev._bridge_responses = {
(2, 0x10, "00"): bytes([0x00, 0x00, 0x03, 0x02, 0x04]) + b"H001",
(2, 0x10, "01"): bytes([0x00, 0x00, 0x03, 0x02, 0x04]) + b"H001", # duplicate → dedup stops
}
fw = _hidpp20.get_firmware_centurion_sub(dev)
assert fw is not None
assert len(fw) == 1
assert fw[0].name == "H001"
assert fw[0].version == "3.02"
def test_centurion_sub_device_serial():
"""get_serial_centurion_sub() queries sub-device serial via bridge."""
dev = fake_hidpp.Device("CENT_SUBSER", True, 2.6, fake_hidpp.r_centurion_headset, centurion=True)
dev._centurion_bridge_index = 3
dev._centurion_sub_features = set()
dev._centurion_sub_indices = {hidpp20_constants.SupportedFeature.CENTURION_DEVICE_INFO: 2}
dev._bridge_responses = {
(2, 0x20, ""): bytes([0x0C]) + b"ABC123DEF456",
}
serial = _hidpp20.get_serial_centurion_sub(dev)
assert serial == "ABC123DEF456"
def test_centurion_sub_device_hardware_info():
"""get_hardware_info_centurion_sub() queries sub-device hardware info via bridge."""
dev = fake_hidpp.Device("CENT_SUBHW", True, 2.6, fake_hidpp.r_centurion_headset, centurion=True)
dev._centurion_bridge_index = 3
dev._centurion_sub_features = set()
dev._centurion_sub_indices = {hidpp20_constants.SupportedFeature.CENTURION_DEVICE_INFO: 2}
dev._bridge_responses = {
(2, 0x00, ""): bytes([0x01, 0x03, 0x0A, 0xF7]),
}
hw_info = _hidpp20.get_hardware_info_centurion_sub(dev)
assert hw_info is not None
model_id, hw_rev, product_id = hw_info
assert model_id == 1
assert hw_rev == 3
assert product_id == 0x0AF7
def test_centurion_kind_inference():
"""Centurion device with 0x06xx audio features infers kind=headset."""
dev = fake_hidpp.Device("CENT_KIND", True, 2.6, fake_hidpp.r_centurion_headset, centurion=True)
dev._centurion_sub_features = {
hidpp20_constants.SupportedFeature.HEADSET_AUDIO_SIDETONE,
hidpp20_constants.SupportedFeature.HEADSET_MIC_MUTE,
}
kind = dev._infer_kind_centurion()
from logitech_receiver import hidpp10_constants
assert kind == hidpp10_constants.DEVICE_KIND.headset
# --- CenturionReceiver tests ---
class FakeCenturionDeviceInfo:
"""Minimal device_info for CenturionReceiver tests."""
def __init__(self, path="/dev/hidraw99", product_id="0AF0", product=None, centurion=True):
self.path = path
self.product_id = product_id
self.product = product
self.centurion = centurion
self.isDevice = True
class FakeLowLevel:
"""Minimal low_level for CenturionReceiver tests."""
def __init__(self, ping_protocol=2.6):
self.ping_protocol = ping_protocol
self.opened_paths = []
self.closed_handles = []
def open_path(self, path):
self.opened_paths.append(path)
return 0x99
def ping(self, handle, number, long_message=False):
return self.ping_protocol
def request(self, handle, devnumber, request_id, *params, **kwargs):
return None
def close(self, handle, *args, **kwargs):
self.closed_handles.append(handle)
return True
def find_paired_node(self, receiver_path, index, timeout):
return None
def test_centurion_receiver_attributes():
"""CenturionReceiver has correct receiver-like attributes."""
info = FakeCenturionDeviceInfo(product="PRO X 2 LIGHTSPEED")
recv = CenturionReceiver(FakeLowLevel(), 0x99, info)
assert recv.kind is None
assert recv.isDevice is False
assert recv.number == 0xFF
assert recv.max_devices == 1
assert recv.may_unpair is False
assert recv.re_pairs is False
assert recv.handle == 0x99
assert recv.path == "/dev/hidraw99"
assert recv.product_id == "0AF0"
assert recv.name == "Centurion Receiver"
assert recv.serial is None
assert recv.pairing is not None
assert recv.pairing.lock_open is False
assert bool(recv) is True
def test_centurion_receiver_container_empty():
"""Empty CenturionReceiver has correct container behavior."""
info = FakeCenturionDeviceInfo()
recv = CenturionReceiver(FakeLowLevel(), 0x99, info)
assert len(recv) == 0
assert recv.count() == 0
assert 1 not in recv
assert list(recv) == []
assert recv.status_string() == "No devices."
def test_centurion_receiver_container_with_device():
"""CenturionReceiver with a child device has correct container behavior."""
info = FakeCenturionDeviceInfo()
recv = CenturionReceiver(FakeLowLevel(), 0x99, info)
# Simulate adding a child device (a simple mock)
class FakeChild:
number = 1
def close(self):
pass
recv._devices[1] = FakeChild()
assert len(recv) == 1
assert recv.count() == 1
assert 1 in recv
assert 2 not in recv
assert recv[1] is not None
assert recv.status_string() == "1 device connected."
with pytest.raises(IndexError):
recv[2]
def test_centurion_receiver_enable_connection_notifications():
"""CenturionReceiver.enable_connection_notifications() returns False."""
info = FakeCenturionDeviceInfo()
recv = CenturionReceiver(FakeLowLevel(), 0x99, info)
assert recv.enable_connection_notifications() is False
assert recv.remaining_pairings() is None
def test_centurion_receiver_device_codename():
"""CenturionReceiver.device_codename() returns USB product name."""
info = FakeCenturionDeviceInfo(product="PRO X 2 LIGHTSPEED")
recv = CenturionReceiver(FakeLowLevel(), 0x99, info)
assert recv.device_codename(1) == "PRO X 2 LIGHTSPEED"
def test_centurion_receiver_close():
"""CenturionReceiver.close() closes handle and clears devices."""
low_level = FakeLowLevel()
info = FakeCenturionDeviceInfo()
recv = CenturionReceiver(low_level, 0x99, info)
class FakeChild:
closed = False
def close(self):
self.closed = True
child = FakeChild()
recv._devices[1] = child
recv.close()
assert recv.handle is None
assert len(recv._devices) == 0
assert child.closed is True
assert 0x99 in low_level.closed_handles
assert bool(recv) is False
def test_centurion_receiver_changed_callback():
"""CenturionReceiver.changed() invokes status_callback."""
info = FakeCenturionDeviceInfo()
recv = CenturionReceiver(FakeLowLevel(), 0x99, info)
calls = []
recv.status_callback = lambda *args, **kwargs: calls.append((args, kwargs))
recv.changed()
assert len(calls) == 1
assert calls[0][0][0] is recv