313 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			313 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
| 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 import hidpp20_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_length",
 | |
|     [
 | |
|         (device_offline, 0),
 | |
|         (device_standard, 3),
 | |
|     ],
 | |
| )
 | |
| def test_hidpp10_get_firmware(device, expected_length):
 | |
|     firmwares = _hidpp10.get_firmware(device)
 | |
| 
 | |
|     assert len(firmwares) == expected_length if expected_length > 0 else firmwares is None
 | |
|     for firmware in firmwares if firmwares is not None else []:
 | |
|         assert firmware.kind in hidpp20_constants.FIRMWARE_KIND
 | |
| 
 | |
| 
 | |
| @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.NOTIFICATION_FLAG.battery_status, hidpp10_constants.NOTIFICATION_FLAG.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.NOTIFICATION_FLAG.battery_status, hidpp10_constants.NOTIFICATION_FLAG.wireless
 | |
|     )
 | |
| 
 | |
|     assert spy_request.call_count == 0
 | |
|     assert result is None
 | |
| 
 | |
| 
 | |
| 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
 |