diff --git a/lib/logitech_receiver/hidpp10.py b/lib/logitech_receiver/hidpp10.py index ae4798b7..da893609 100644 --- a/lib/logitech_receiver/hidpp10.py +++ b/lib/logitech_receiver/hidpp10.py @@ -191,7 +191,7 @@ class Hidpp10: def get_notification_flags(self, device: Device): return NotificationFlag(self._get_register(device, Registers.NOTIFICATIONS)) - def set_notification_flags(self, device: Device, *flag_bits: NotificationFlag): + def set_notification_flags(self, device: Device, *flag_bits: NotificationFlag) -> None: assert device is not None # Avoid a call if the device is not online, diff --git a/lib/logitech_receiver/hidpp20.py b/lib/logitech_receiver/hidpp20.py index 05295846..db33d61a 100644 --- a/lib/logitech_receiver/hidpp20.py +++ b/lib/logitech_receiver/hidpp20.py @@ -1604,7 +1604,7 @@ class Hidpp20: except Exception: return None - def get_name(self, device: Device): + def get_name(self, device: Device) -> str: """Reads a device's name. :returns: a string with the device name, or ``None`` if the device is not @@ -1726,7 +1726,7 @@ class Hidpp20: if SupportedFeature.ONBOARD_PROFILES in device.features: return OnboardProfiles.from_device(device) - def get_mouse_pointer_info(self, device: Device): + def get_mouse_pointer_info(self, device: Device) -> dict[str, Any]: pointer_info = device.feature_request(SupportedFeature.MOUSE_POINTER) if pointer_info: dpi, flags = struct.unpack("!HB", pointer_info[:3]) @@ -1740,7 +1740,7 @@ class Hidpp20: "suggest_vertical_orientation": suggest_vertical_orientation, } - def get_vertical_scrolling_info(self, device: Device): + def get_vertical_scrolling_info(self, device: Device) -> dict[str, Any]: vertical_scrolling_info = device.feature_request(SupportedFeature.VERTICAL_SCROLLING) if vertical_scrolling_info: roller, ratchet, lines = struct.unpack("!BBB", vertical_scrolling_info[:3]) @@ -1756,13 +1756,13 @@ class Hidpp20: )[roller] return {"roller": roller_type, "ratchet": ratchet, "lines": lines} - def get_hi_res_scrolling_info(self, device: Device): + def get_hi_res_scrolling_info(self, device: Device) -> tuple[int, int]: hi_res_scrolling_info = device.feature_request(SupportedFeature.HI_RES_SCROLLING) if hi_res_scrolling_info: mode, resolution = struct.unpack("!BB", hi_res_scrolling_info[:2]) return mode, resolution - def get_pointer_speed_info(self, device: Device): + def get_pointer_speed_info(self, device: Device) -> float: pointer_speed_info = device.feature_request(SupportedFeature.POINTER_SPEED) if pointer_speed_info: pointer_speed_hi, pointer_speed_lo = struct.unpack("!BB", pointer_speed_info[:2]) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..4b95e552 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +import pytest + +from fakes import hidpp10_device + +from .fakes import hidpp20_device + + +@pytest.fixture +def device_hidpp10(): + yield hidpp10_device.Hidpp10Device() + + +@pytest.fixture +def device(): + yield hidpp20_device.Hidpp20Device() diff --git a/tests/fakes/__init__.py b/tests/fakes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/fakes/hidpp10_device.py b/tests/fakes/hidpp10_device.py new file mode 100644 index 00000000..a25ffca6 --- /dev/null +++ b/tests/fakes/hidpp10_device.py @@ -0,0 +1,18 @@ +from logitech_receiver.hidpp10_constants import Registers + + +class Hidpp10Device: + def __init__(self): + self._iteration = 0 + + @property + def kind(self): + pass + + def request(self, request_id, *params, no_reply=False): + if request_id == 0x8100 + Registers.BATTERY_STATUS: + return b"fff" + elif request_id == 0x8000: + return bytes([0x10, 0x10, 0x00]) + + raise RuntimeError(f"Unsupported feature: {request_id:04X}") diff --git a/tests/fakes/hidpp20_device.py b/tests/fakes/hidpp20_device.py new file mode 100644 index 00000000..9b1f1faa --- /dev/null +++ b/tests/fakes/hidpp20_device.py @@ -0,0 +1,62 @@ +from logitech_receiver.hidpp20_constants import SupportedFeature + + +class Hidpp20Device: + def __init__(self): + self._iteration = 0 + + def feature_request(self, feature, function: int = 0x00, *params, no_reply: bool = False) -> bytes: + self._iteration += 1 + if feature == SupportedFeature.DEVICE_FW_VERSION: + if function == 0x00: + self._iteration = 1 + return bytes([0x02, 0xFF, 0xFF]) + elif self._iteration == 2 and function == 0x10: + return bytes.fromhex("01414243030401000101000102030405") + elif self._iteration == 3: + self._iteration = 0 + return bytes.fromhex("02414243030401000101000102030405") + elif feature == SupportedFeature.DEVICE_NAME: + if function == 0x00: + self._iteration = 1 + return bytes([0x12]) + elif function == 0x10: + if self._iteration == 2: + return bytes.fromhex("4142434445464748494A4B4C4D4E4F") + elif self._iteration == 3: + return bytes.fromhex("505152530000000000000000000000") + elif function == 0x20: + keyboard = 0x00 + return bytes([keyboard]) + elif feature == SupportedFeature.DEVICE_FRIENDLY_NAME: + if function == 0x00: + self._iteration = 1 + return bytes([0x12]) + elif function == 0x10: + if self._iteration == 2: + return bytes.fromhex("004142434445464748494A4B4C4D4E") + elif self._iteration == 3: + return bytes.fromhex("0E4F50515253000000000000000000") + elif feature == SupportedFeature.BATTERY_STATUS: + if function == 0x00: + return bytes.fromhex("502000FFFF") + elif feature == SupportedFeature.VERTICAL_SCROLLING: + roller_type = 0x01 + num_of_ratchet_by_turn = 0x08 + scroll_lines = 0x0C + return bytes([roller_type, num_of_ratchet_by_turn, scroll_lines]) + elif feature == SupportedFeature.HI_RES_SCROLLING: + mode = 0x01 + resolution = 0x02 + return bytes([mode, resolution]) + elif feature == SupportedFeature.MOUSE_POINTER: + sensor_resolution_msb = 0x01 + sensor_resolution_lsb = 0x00 + flags = 0x0A + return bytes([sensor_resolution_msb, sensor_resolution_lsb, flags]) + elif feature == SupportedFeature.POINTER_SPEED: + pointer_speed_high = 0x01 + pointer_speed_low = 0x03 + return bytes([pointer_speed_high, pointer_speed_low]) + + raise RuntimeError(f"Unsupported feature: {feature.name}, func=0x{function:02X}") diff --git a/tests/logitech_receiver/test_hidpp10.py b/tests/logitech_receiver/test_hidpp10.py index b642608a..71562abc 100644 --- a/tests/logitech_receiver/test_hidpp10.py +++ b/tests/logitech_receiver/test_hidpp10.py @@ -1,12 +1,3 @@ -from dataclasses import dataclass -from dataclasses import field -from typing import Any -from typing import List -from typing import Optional - -import pytest - -from logitech_receiver import common from logitech_receiver import hidpp10 from logitech_receiver import hidpp10_constants from logitech_receiver.hidpp10_constants import Registers @@ -14,327 +5,23 @@ from logitech_receiver.hidpp10_constants import Registers _hidpp10 = hidpp10.Hidpp10() -@dataclass -class Response: - response: Optional[str] - request_id: int - params: Any - - -@dataclass -class Device: - name: str = "Device" - online: bool = True - kind: str = "fake" - protocol: float = 1.0 - isDevice: bool = False # incorrect, but useful here - registers: List[Registers] = field(default_factory=list) - responses: List[Response] = field(default_factory=list) - - def request(self, id, params=None, no_reply=False): - if params is None: - params = [] - print("REQUEST ", self.name, hex(id), params) - for r in self.responses: - if id == r.request_id and params == r.params: - print("RESPONSE", self.name, hex(r.request_id), r.params, r.response) - return bytes.fromhex(r.response) if r.response is not None else None - - -device_offline = Device("OFFLINE", False) -device_leds = Device("LEDS", True, registers=[Registers.THREE_LEDS, Registers.BATTERY_STATUS]) -device_features = Device("FEATURES", True, protocol=4.5) - -registers_standard = [Registers.BATTERY_STATUS, Registers.FIRMWARE] -responses_standard = [ - Response("555555", 0x8100 | Registers.BATTERY_STATUS, 0x00), - Response("666666", 0x8100 | Registers.BATTERY_STATUS, 0x10), - Response("777777", 0x8000 | Registers.BATTERY_STATUS, 0x00), - Response("888888", 0x8000 | Registers.BATTERY_STATUS, 0x10), - Response("052100", 0x8100 | Registers.BATTERY_STATUS, []), - Response("ABCDEF", 0x8100 | Registers.FIRMWARE, 0x01), - Response("ABCDEF", 0x8100 | Registers.FIRMWARE, 0x02), - Response("ABCDEF", 0x8100 | Registers.FIRMWARE, 0x03), - Response("ABCDEF", 0x8100 | Registers.FIRMWARE, 0x04), - Response("000900", 0x8100 | Registers.NOTIFICATIONS, []), - Response("101010", 0x8100 | Registers.MOUSE_BUTTON_FLAGS, []), - Response("010101", 0x8100 | Registers.KEYBOARD_FN_SWAP, []), - Response("020202", 0x8100 | Registers.DEVICES_CONFIGURATION, []), - Response("030303", 0x8000 | Registers.DEVICES_CONFIGURATION, 0x00), -] -device_standard = Device("STANDARD", True, registers=registers_standard, responses=responses_standard) - - -@pytest.mark.parametrize( - "device, register, param, expected_result", - [ - (device_offline, Registers.THREE_LEDS, 0x00, None), - (device_standard, Registers.THREE_LEDS, 0x00, None), - (device_standard, Registers.BATTERY_STATUS, 0x00, "555555"), - (device_standard, Registers.BATTERY_STATUS, 0x10, "666666"), - ], -) -def test_read_register(device, register, param, expected_result, mocker): - spy_request = mocker.spy(device, "request") - - result = hidpp10.read_register(device, register, param) - - assert result == (bytes.fromhex(expected_result) if expected_result else None) - spy_request.assert_called_once_with(0x8100 | register, param) - - -@pytest.mark.parametrize( - "device, register, param, expected_result", - [ - (device_offline, Registers.THREE_LEDS, 0x00, None), - (device_standard, Registers.THREE_LEDS, 0x00, None), - (device_standard, Registers.BATTERY_STATUS, 0x00, "777777"), - (device_standard, Registers.BATTERY_STATUS, 0x10, "888888"), - ], -) -def test_write_register(device, register, param, expected_result, mocker): - spy_request = mocker.spy(device, "request") - - result = hidpp10.write_register(device, register, param) - - assert result == (bytes.fromhex(expected_result) if expected_result else None) - spy_request.assert_called_once_with(0x8000 | register, param) - - -def device_charge(name, response): - responses = [Response(response, 0x8100 | Registers.BATTERY_CHARGE, [])] - return Device(name, registers=[], responses=responses) - - -device_charge1 = device_charge("DISCHARGING", "550030") -device_charge2 = device_charge("RECHARGING", "440050") -device_charge3 = device_charge("FULL", "600090") -device_charge4 = device_charge("OTHER", "220000") - - -def device_status(name, response): - responses = [Response(response, 0x8100 | Registers.BATTERY_STATUS, [])] - return Device(name, registers=[], responses=responses) - - -device_status1 = device_status("FULL", "072200") -device_status2 = device_status("GOOD", "052100") -device_status3 = device_status("LOW", "032200") -device_status4 = device_status("CRITICAL", "010100") -device_status5 = device_status("EMPTY", "000000") -device_status6 = device_status("NOSTATUS", "002200") - - -@pytest.mark.parametrize( - "device, expected_result, expected_register", - [ - (device_offline, None, None), - (device_features, None, None), - (device_leds, None, None), - ( - device_standard, - common.Battery(common.BatteryLevelApproximation.GOOD, None, common.BatteryStatus.RECHARGING, None), - Registers.BATTERY_STATUS, - ), - (device_charge1, common.Battery(0x55, None, common.BatteryStatus.DISCHARGING, None), Registers.BATTERY_CHARGE), - ( - device_charge2, - common.Battery(0x44, None, common.BatteryStatus.RECHARGING, None), - Registers.BATTERY_CHARGE, - ), - ( - device_charge3, - common.Battery(0x60, None, common.BatteryStatus.FULL, None), - Registers.BATTERY_CHARGE, - ), - (device_charge4, common.Battery(0x22, None, None, None), Registers.BATTERY_CHARGE), - ( - device_status1, - common.Battery(common.BatteryLevelApproximation.FULL, None, common.BatteryStatus.FULL, None), - Registers.BATTERY_STATUS, - ), - ( - device_status2, - common.Battery(common.BatteryLevelApproximation.GOOD, None, common.BatteryStatus.RECHARGING, None), - Registers.BATTERY_STATUS, - ), - ( - device_status3, - common.Battery(common.BatteryLevelApproximation.LOW, None, common.BatteryStatus.FULL, None), - Registers.BATTERY_STATUS, - ), - ( - device_status4, - common.Battery(common.BatteryLevelApproximation.CRITICAL, None, None, None), - Registers.BATTERY_STATUS, - ), - ( - device_status5, - common.Battery(common.BatteryLevelApproximation.EMPTY, None, common.BatteryStatus.DISCHARGING, None), - Registers.BATTERY_STATUS, - ), - ( - device_status6, - common.Battery(None, None, common.BatteryStatus.FULL, None), - Registers.BATTERY_STATUS, - ), - ], -) -def test_hidpp10_get_battery(device, expected_result, expected_register): - result = _hidpp10.get_battery(device) - - assert result == expected_result - if expected_register is not None: - assert expected_register in device.registers - - -@pytest.mark.parametrize( - "device, expected_firmwares", - [ - (device_offline, []), - ( - device_standard, - [ - common.FirmwareKind.Firmware, - common.FirmwareKind.Bootloader, - common.FirmwareKind.Other, - ], - ), - ], -) -def test_hidpp10_get_firmware(device, expected_firmwares): - firmwares = _hidpp10.get_firmware(device) - - if not expected_firmwares: - assert firmwares is None - else: - firmware_types = [firmware.kind for firmware in firmwares] - assert firmware_types == expected_firmwares - assert len(firmwares) == len(expected_firmwares) - - -@pytest.mark.parametrize( - "device, level, charging, warning, p1, p2", - [ - (device_leds, common.BatteryLevelApproximation.EMPTY, False, False, 0x33, 0x00), - (device_leds, common.BatteryLevelApproximation.CRITICAL, False, False, 0x22, 0x00), - (device_leds, common.BatteryLevelApproximation.LOW, False, False, 0x20, 0x00), - (device_leds, common.BatteryLevelApproximation.GOOD, False, False, 0x20, 0x02), - (device_leds, common.BatteryLevelApproximation.FULL, False, False, 0x20, 0x22), - (device_leds, None, True, False, 0x30, 0x33), - (device_leds, None, False, True, 0x02, 0x00), - (device_leds, None, False, False, 0x11, 0x11), - ], -) -def test_set_3leds(device, level, charging, warning, p1, p2, mocker): - spy_request = mocker.spy(device, "request") - - _hidpp10.set_3leds(device, level, charging, warning) - - spy_request.assert_called_once_with(0x8000 | Registers.THREE_LEDS, p1, p2) - - -@pytest.mark.parametrize("device", [device_offline, device_features]) -def test_set_3leds_missing(device, mocker): - spy_request = mocker.spy(device, "request") - - _hidpp10.set_3leds(device) - - assert spy_request.call_count == 0 - - -@pytest.mark.parametrize("device", [device_standard]) -def test_get_notification_flags(device): - result = _hidpp10.get_notification_flags(device) +def test_read_register(device_hidpp10): + result = hidpp10.read_register( + device_hidpp10, + register=Registers.BATTERY_STATUS, + ) assert result == hidpp10_constants.NotificationFlag(int("000900", 16)) -def test_set_notification_flags(mocker): - device = device_standard - spy_request = mocker.spy(device, "request") +def test_set_notification_flags(mocker, device_hidpp10): + spy_request = mocker.spy(device_hidpp10, "request") result = _hidpp10.set_notification_flags( - device, hidpp10_constants.NotificationFlag.BATTERY_STATUS, hidpp10_constants.NotificationFlag.WIRELESS + device_hidpp10, + hidpp10_constants.NotificationFlag.BATTERY_STATUS, + hidpp10_constants.NotificationFlag.WIRELESS, ) spy_request.assert_called_once_with(0x8000 | Registers.NOTIFICATIONS, b"\x10\x01\x00") assert result is not None - - -def test_set_notification_flags_bad(mocker): - device = device_features - spy_request = mocker.spy(device, "request") - - result = _hidpp10.set_notification_flags( - device, hidpp10_constants.NotificationFlag.BATTERY_STATUS, hidpp10_constants.NotificationFlag.WIRELESS - ) - - assert spy_request.call_count == 0 - assert result is None - - -@pytest.mark.parametrize( - "flag_bits, expected_names", - [ - (None, ""), - (0x0, "none"), - (0x009020, "multi touch\n unknown:008020"), - (0x080000, "mouse extra buttons"), - ( - 0x080000 + 0x000400, - ("link quality\n mouse extra buttons"), - ), - ], -) -def test_notification_flag_str(flag_bits, expected_names): - flag_names = hidpp10_constants.flags_to_str(flag_bits, fallback="none") - - assert flag_names == expected_names - - -def test_get_device_features(): - result = _hidpp10.get_device_features(device_standard) - - assert result == int("101010", 16) - - -@pytest.mark.parametrize( - "device, register, expected_result", - [ - (device_standard, Registers.BATTERY_STATUS, "052100"), - (device_standard, Registers.MOUSE_BUTTON_FLAGS, "101010"), - (device_standard, Registers.KEYBOARD_ILLUMINATION, None), - (device_features, Registers.KEYBOARD_ILLUMINATION, None), - ], -) -def test_get_register(device, register, expected_result): - result = _hidpp10._get_register(device, register) - - assert result == (int(expected_result, 16) if expected_result is not None else None) - - -@pytest.mark.parametrize( - "device, expected_result", - [ - (device_standard, 2), - (device_features, None), - ], -) -def test_get_configuration_pending_flags(device, expected_result): - result = hidpp10.get_configuration_pending_flags(device) - - assert result == expected_result - - -@pytest.mark.parametrize( - "device, expected_result", - [ - (device_standard, True), - (device_features, False), - ], -) -def test_set_configuration_pending_flags(device, expected_result): - result = hidpp10.set_configuration_pending_flags(device, 0x00) - - assert result == expected_result diff --git a/tests/logitech_receiver/test_hidpp10_simple.py b/tests/logitech_receiver/test_hidpp10_simple.py new file mode 100644 index 00000000..1c39b0c0 --- /dev/null +++ b/tests/logitech_receiver/test_hidpp10_simple.py @@ -0,0 +1,340 @@ +from dataclasses import dataclass +from dataclasses import field +from typing import Any +from typing import List +from typing import Optional + +import pytest + +from logitech_receiver import common +from logitech_receiver import hidpp10 +from logitech_receiver import hidpp10_constants +from logitech_receiver.hidpp10_constants import Registers + +_hidpp10 = hidpp10.Hidpp10() + + +@dataclass +class Response: + response: Optional[str] + request_id: int + params: Any + + +@dataclass +class Device: + name: str = "Device" + online: bool = True + kind: str = "fake" + protocol: float = 1.0 + isDevice: bool = False # incorrect, but useful here + registers: List[Registers] = field(default_factory=list) + responses: List[Response] = field(default_factory=list) + + def request(self, id, params=None, no_reply=False): + if params is None: + params = [] + print("REQUEST ", self.name, hex(id), params) + for r in self.responses: + if id == r.request_id and params == r.params: + print("RESPONSE", self.name, hex(r.request_id), r.params, r.response) + return bytes.fromhex(r.response) if r.response is not None else None + + +device_offline = Device("OFFLINE", False) +device_leds = Device("LEDS", True, registers=[Registers.THREE_LEDS, Registers.BATTERY_STATUS]) +device_features = Device("FEATURES", True, protocol=4.5) + +registers_standard = [Registers.BATTERY_STATUS, Registers.FIRMWARE] +responses_standard = [ + Response("555555", 0x8100 | Registers.BATTERY_STATUS, 0x00), + Response("666666", 0x8100 | Registers.BATTERY_STATUS, 0x10), + Response("777777", 0x8000 | Registers.BATTERY_STATUS, 0x00), + Response("888888", 0x8000 | Registers.BATTERY_STATUS, 0x10), + Response("052100", 0x8100 | Registers.BATTERY_STATUS, []), + Response("ABCDEF", 0x8100 | Registers.FIRMWARE, 0x01), + Response("ABCDEF", 0x8100 | Registers.FIRMWARE, 0x02), + Response("ABCDEF", 0x8100 | Registers.FIRMWARE, 0x03), + Response("ABCDEF", 0x8100 | Registers.FIRMWARE, 0x04), + Response("000900", 0x8100 | Registers.NOTIFICATIONS, []), + Response("101010", 0x8100 | Registers.MOUSE_BUTTON_FLAGS, []), + Response("010101", 0x8100 | Registers.KEYBOARD_FN_SWAP, []), + Response("020202", 0x8100 | Registers.DEVICES_CONFIGURATION, []), + Response("030303", 0x8000 | Registers.DEVICES_CONFIGURATION, 0x00), +] +device_standard = Device("STANDARD", True, registers=registers_standard, responses=responses_standard) + + +@pytest.mark.parametrize( + "device, register, param, expected_result", + [ + (device_offline, Registers.THREE_LEDS, 0x00, None), + (device_standard, Registers.THREE_LEDS, 0x00, None), + (device_standard, Registers.BATTERY_STATUS, 0x00, "555555"), + (device_standard, Registers.BATTERY_STATUS, 0x10, "666666"), + ], +) +def test_read_register(device, register, param, expected_result, mocker): + spy_request = mocker.spy(device, "request") + + result = hidpp10.read_register(device, register, param) + + assert result == (bytes.fromhex(expected_result) if expected_result else None) + spy_request.assert_called_once_with(0x8100 | register, param) + + +@pytest.mark.parametrize( + "device, register, param, expected_result", + [ + (device_offline, Registers.THREE_LEDS, 0x00, None), + (device_standard, Registers.THREE_LEDS, 0x00, None), + (device_standard, Registers.BATTERY_STATUS, 0x00, "777777"), + (device_standard, Registers.BATTERY_STATUS, 0x10, "888888"), + ], +) +def test_write_register(device, register, param, expected_result, mocker): + spy_request = mocker.spy(device, "request") + + result = hidpp10.write_register(device, register, param) + + assert result == (bytes.fromhex(expected_result) if expected_result else None) + spy_request.assert_called_once_with(0x8000 | register, param) + + +def device_charge(name, response): + responses = [Response(response, 0x8100 | Registers.BATTERY_CHARGE, [])] + return Device(name, registers=[], responses=responses) + + +device_charge1 = device_charge("DISCHARGING", "550030") +device_charge2 = device_charge("RECHARGING", "440050") +device_charge3 = device_charge("FULL", "600090") +device_charge4 = device_charge("OTHER", "220000") + + +def device_status(name, response): + responses = [Response(response, 0x8100 | Registers.BATTERY_STATUS, [])] + return Device(name, registers=[], responses=responses) + + +device_status1 = device_status("FULL", "072200") +device_status2 = device_status("GOOD", "052100") +device_status3 = device_status("LOW", "032200") +device_status4 = device_status("CRITICAL", "010100") +device_status5 = device_status("EMPTY", "000000") +device_status6 = device_status("NOSTATUS", "002200") + + +@pytest.mark.parametrize( + "device, expected_result, expected_register", + [ + (device_offline, None, None), + (device_features, None, None), + (device_leds, None, None), + ( + device_standard, + common.Battery(common.BatteryLevelApproximation.GOOD, None, common.BatteryStatus.RECHARGING, None), + Registers.BATTERY_STATUS, + ), + (device_charge1, common.Battery(0x55, None, common.BatteryStatus.DISCHARGING, None), Registers.BATTERY_CHARGE), + ( + device_charge2, + common.Battery(0x44, None, common.BatteryStatus.RECHARGING, None), + Registers.BATTERY_CHARGE, + ), + ( + device_charge3, + common.Battery(0x60, None, common.BatteryStatus.FULL, None), + Registers.BATTERY_CHARGE, + ), + (device_charge4, common.Battery(0x22, None, None, None), Registers.BATTERY_CHARGE), + ( + device_status1, + common.Battery(common.BatteryLevelApproximation.FULL, None, common.BatteryStatus.FULL, None), + Registers.BATTERY_STATUS, + ), + ( + device_status2, + common.Battery(common.BatteryLevelApproximation.GOOD, None, common.BatteryStatus.RECHARGING, None), + Registers.BATTERY_STATUS, + ), + ( + device_status3, + common.Battery(common.BatteryLevelApproximation.LOW, None, common.BatteryStatus.FULL, None), + Registers.BATTERY_STATUS, + ), + ( + device_status4, + common.Battery(common.BatteryLevelApproximation.CRITICAL, None, None, None), + Registers.BATTERY_STATUS, + ), + ( + device_status5, + common.Battery(common.BatteryLevelApproximation.EMPTY, None, common.BatteryStatus.DISCHARGING, None), + Registers.BATTERY_STATUS, + ), + ( + device_status6, + common.Battery(None, None, common.BatteryStatus.FULL, None), + Registers.BATTERY_STATUS, + ), + ], +) +def test_hidpp10_get_battery(device, expected_result, expected_register): + result = _hidpp10.get_battery(device) + + assert result == expected_result + if expected_register is not None: + assert expected_register in device.registers + + +@pytest.mark.parametrize( + "device, expected_firmwares", + [ + (device_offline, []), + ( + device_standard, + [ + common.FirmwareKind.Firmware, + common.FirmwareKind.Bootloader, + common.FirmwareKind.Other, + ], + ), + ], +) +def test_hidpp10_get_firmware(device, expected_firmwares): + firmwares = _hidpp10.get_firmware(device) + + if not expected_firmwares: + assert firmwares is None + else: + firmware_types = [firmware.kind for firmware in firmwares] + assert firmware_types == expected_firmwares + assert len(firmwares) == len(expected_firmwares) + + +@pytest.mark.parametrize( + "device, level, charging, warning, p1, p2", + [ + (device_leds, common.BatteryLevelApproximation.EMPTY, False, False, 0x33, 0x00), + (device_leds, common.BatteryLevelApproximation.CRITICAL, False, False, 0x22, 0x00), + (device_leds, common.BatteryLevelApproximation.LOW, False, False, 0x20, 0x00), + (device_leds, common.BatteryLevelApproximation.GOOD, False, False, 0x20, 0x02), + (device_leds, common.BatteryLevelApproximation.FULL, False, False, 0x20, 0x22), + (device_leds, None, True, False, 0x30, 0x33), + (device_leds, None, False, True, 0x02, 0x00), + (device_leds, None, False, False, 0x11, 0x11), + ], +) +def test_set_3leds(device, level, charging, warning, p1, p2, mocker): + spy_request = mocker.spy(device, "request") + + _hidpp10.set_3leds(device, level, charging, warning) + + spy_request.assert_called_once_with(0x8000 | Registers.THREE_LEDS, p1, p2) + + +@pytest.mark.parametrize("device", [device_offline, device_features]) +def test_set_3leds_missing(device, mocker): + spy_request = mocker.spy(device, "request") + + _hidpp10.set_3leds(device) + + assert spy_request.call_count == 0 + + +@pytest.mark.parametrize("device", [device_standard]) +def test_get_notification_flags(device): + result = _hidpp10.get_notification_flags(device) + + assert result == int("000900", 16) + + +def test_set_notification_flags(mocker): + device = device_standard + spy_request = mocker.spy(device, "request") + + result = _hidpp10.set_notification_flags( + device, hidpp10_constants.NotificationFlag.BATTERY_STATUS, hidpp10_constants.NotificationFlag.WIRELESS + ) + + spy_request.assert_called_once_with(0x8000 | Registers.NOTIFICATIONS, b"\x10\x01\x00") + assert result is not None + + +def test_set_notification_flags_bad(mocker): + device = device_features + spy_request = mocker.spy(device, "request") + + result = _hidpp10.set_notification_flags( + device, hidpp10_constants.NotificationFlag.BATTERY_STATUS, hidpp10_constants.NotificationFlag.WIRELESS + ) + + assert spy_request.call_count == 0 + assert result is None + + +@pytest.mark.parametrize( + "flag_bits, expected_names", + [ + (None, ""), + (0x0, "none"), + (0x009020, "multi touch\n unknown:008020"), + (0x080000, "mouse extra buttons"), + ( + 0x080000 + 0x000400, + ("link quality\n mouse extra buttons"), + ), + ], +) +def test_notification_flag_str(flag_bits, expected_names): + flag_names = hidpp10_constants.flags_to_str(flag_bits, fallback="none") + + assert flag_names == expected_names + + +def test_get_device_features(): + result = _hidpp10.get_device_features(device_standard) + + assert result == int("101010", 16) + + +@pytest.mark.parametrize( + "device, register, expected_result", + [ + (device_standard, Registers.BATTERY_STATUS, "052100"), + (device_standard, Registers.MOUSE_BUTTON_FLAGS, "101010"), + (device_standard, Registers.KEYBOARD_ILLUMINATION, None), + (device_features, Registers.KEYBOARD_ILLUMINATION, None), + ], +) +def test_get_register(device, register, expected_result): + result = _hidpp10._get_register(device, register) + + assert result == (int(expected_result, 16) if expected_result is not None else None) + + +@pytest.mark.parametrize( + "device, expected_result", + [ + (device_standard, 2), + (device_features, None), + ], +) +def test_get_configuration_pending_flags(device, expected_result): + result = hidpp10.get_configuration_pending_flags(device) + + assert result == expected_result + + +@pytest.mark.parametrize( + "device, expected_result", + [ + (device_standard, True), + (device_features, False), + ], +) +def test_set_configuration_pending_flags(device, expected_result): + result = hidpp10.set_configuration_pending_flags(device, 0x00) + + assert result == expected_result diff --git a/tests/logitech_receiver/test_hidpp20.py b/tests/logitech_receiver/test_hidpp20.py new file mode 100644 index 00000000..abd8c20e --- /dev/null +++ b/tests/logitech_receiver/test_hidpp20.py @@ -0,0 +1,83 @@ +from logitech_receiver import common +from logitech_receiver import hidpp20 +from logitech_receiver.common import FirmwareKind +from logitech_receiver.hidpp20_constants import SupportedFeature + +_hidpp20 = hidpp20.Hidpp20() + + +def test_get_firmware(device): + result = _hidpp20.get_firmware(device) + + assert result == ( + common.FirmwareInfo( + kind=FirmwareKind.Bootloader, + name="ABC", + version="03.04.B0100", + extras=b"\x01\x00\x01\x02\x03\x04\x05", + ), + common.FirmwareInfo( + kind=FirmwareKind.Hardware, + name="", + version="65", + extras=None, + ), + ) + + +def test_get_kind(device): + result = _hidpp20.get_kind(device) + + assert result == "keyboard" + assert result == 1 + + +def test_get_name(device): + result = _hidpp20.get_name(device) + + assert result == "ABCDEFGHIJKLMNOPQR" + + +def test_get_friendly_name(device): + result = _hidpp20.get_friendly_name(device) + + assert result == "ABCDEFGHIJKLMNOPQR" + + +def test_get_battery_status(device): + feature, battery = _hidpp20.get_battery_status(device) + + assert feature == SupportedFeature.BATTERY_STATUS + assert battery.level == 80 + assert battery.next_level == 32 + assert battery.status == common.BatteryStatus.DISCHARGING + + +def test_get_vertical_scrolling_info(device): + result = _hidpp20.get_vertical_scrolling_info(device) + + assert result == {"roller": "standard", "ratchet": 8, "lines": 12} + + +def test_get_high_resolution_scrolling_info(device): + mode, resolution = _hidpp20.get_hi_res_scrolling_info(device) + + assert mode == 0x1 + assert resolution == 0x2 + + +def test_get_mouse_pointer_info(device): + result = _hidpp20.get_mouse_pointer_info(device) + + assert result == { + "dpi": 0x100, + "acceleration": "med", + "suggest_os_ballistics": False, + "suggest_vertical_orientation": True, + } + + +def test_get_pointer_speed_info(device): + result = _hidpp20.get_pointer_speed_info(device) + + assert result == 0x0103 / 256