Merge branch 'fix/hidpp-mouse-detected-as-keyboard' into 'master'

linux: up-device-supply: Fix mouse with mixed input capabilities classified as keyboard

Closes #262 and #215

See merge request upower/upower!312
This commit is contained in:
Miroslav Šuvada 2026-04-27 04:05:55 +00:00
commit 8ca7ce0ff3
2 changed files with 627 additions and 2 deletions

View file

@ -4124,7 +4124,14 @@ class Tests(dbusmock.DBusTestCase):
"/devices/pci0000:00/0000:00:14.0/usb3/3-10/3-10:1.2/0003:046D:C52B.0009/0003:046D:4101.000A/input/input22",
parent,
[],
["DEVNAME", "input/mouse3", "ID_INPUT_MOUSE", "1"],
[
"DEVNAME",
"input/mouse3",
"ID_INPUT_MOUSE",
"1",
"ID_INPUT_KEYBOARD",
"1",
],
)
self.testbed.add_device(
@ -4174,6 +4181,602 @@ class Tests(dbusmock.DBusTestCase):
)
self.stop_daemon()
def test_hidpp_mouse_with_keyboard_interface(self):
"""HID++ mouse with extra keyboard interface stays classified as mouse"""
parent = self.testbed.add_device(
"usb", "/devices/pci0000:00/0000:00:14.0/usb3/3-10/3-10:1.2", None, [], []
)
parent = self.testbed.add_device(
"hid",
"/devices/pci0000:00/0000:00:14.0/usb3/3-10/3-10:1.2/0003:046D:C52B.0009",
parent,
[],
[],
)
dev = self.testbed.add_device(
"hid",
"/devices/pci0000:00/0000:00:14.0/usb3/3-10/3-10:1.2/0003:046D:C52B.0009/0003:046D:4101.000A",
parent,
[],
[],
)
parent = dev
self.testbed.add_device(
"input",
"/devices/pci0000:00/0000:00:14.0/usb3/3-10/3-10:1.2/0003:046D:C52B.0009/0003:046D:4101.000A/input/input22",
parent,
[],
[
"DEVNAME",
"input/mouse3",
"ID_INPUT_MOUSE",
"1",
"ID_INPUT_KEYBOARD",
"1",
],
)
self.testbed.add_device(
"input",
"/devices/pci0000:00/0000:00:14.0/usb3/3-10/3-10:1.2/0003:046D:C52B.0009/0003:046D:4101.000A/input/input23",
parent,
[],
["DEVNAME", "input/event7", "ID_INPUT_KEYBOARD", "1"],
)
self.testbed.add_device(
"power_supply",
"/devices/pci0000:00/0000:00:14.0/usb3/3-10/3-10:1.2/0003:046D:C52B.0009/0003:046D:4101.000A/power_supply/hidpp_battery_3",
parent,
[
"type",
"Battery",
"scope",
"Device",
"present",
"1",
"online",
"1",
"status",
"Discharging",
"capacity",
"30",
"serial_number",
"654321",
"model_name",
"Fancy Logitech mouse",
],
[],
)
self.start_daemon()
devs = self.proxy.EnumerateDevices()
self.assertEqual(len(devs), 1)
mousebat0_up = devs[0]
self.assertEqual(
self.get_dbus_dev_property(mousebat0_up, "Model"), "Fancy Logitech mouse"
)
self.assertAlmostEqual(
self.get_dbus_dev_property(mousebat0_up, "Percentage"), 30.0
)
self.assertEqual(self.get_dbus_dev_property(mousebat0_up, "PowerSupply"), False)
self.assertEqual(
self.get_dbus_dev_property(mousebat0_up, "Type"), UP_DEVICE_KIND_MOUSE
)
self.assertEqual(self.get_dbus_dev_property(mousebat0_up, "Serial"), "654321")
self.assertEqual(self.get_dbus_property("OnBattery"), False)
self.assertEqual(
self.get_dbus_display_property("WarningLevel"), UP_DEVICE_LEVEL_NONE
)
self.stop_daemon()
def test_hidpp_mouse_with_keyboard_interface_reverse_discovery(self):
"""HID++ mouse remains a mouse when keyboard sibling appears first"""
parent = self.testbed.add_device(
"usb", "/devices/pci0000:00/0000:00:14.0/usb3/3-10/3-10:1.2", None, [], []
)
parent = self.testbed.add_device(
"hid",
"/devices/pci0000:00/0000:00:14.0/usb3/3-10/3-10:1.2/0003:046D:C52B.0009",
parent,
[],
[],
)
dev = self.testbed.add_device(
"hid",
"/devices/pci0000:00/0000:00:14.0/usb3/3-10/3-10:1.2/0003:046D:C52B.0009/0003:046D:4101.000A",
parent,
[],
[],
)
parent = dev
self.testbed.add_device(
"input",
"/devices/pci0000:00/0000:00:14.0/usb3/3-10/3-10:1.2/0003:046D:C52B.0009/0003:046D:4101.000A/input/input22",
parent,
[],
["DEVNAME", "input/event7", "ID_INPUT_KEYBOARD", "1"],
)
self.testbed.add_device(
"input",
"/devices/pci0000:00/0000:00:14.0/usb3/3-10/3-10:1.2/0003:046D:C52B.0009/0003:046D:4101.000A/input/input23",
parent,
[],
[
"DEVNAME",
"input/mouse3",
"ID_INPUT_MOUSE",
"1",
"ID_INPUT_KEYBOARD",
"1",
],
)
self.testbed.add_device(
"power_supply",
"/devices/pci0000:00/0000:00:14.0/usb3/3-10/3-10:1.2/0003:046D:C52B.0009/0003:046D:4101.000A/power_supply/hidpp_battery_3",
parent,
[
"type",
"Battery",
"scope",
"Device",
"present",
"1",
"online",
"1",
"status",
"Discharging",
"capacity",
"30",
"serial_number",
"654321",
"model_name",
"Fancy Logitech mouse",
],
[],
)
self.start_daemon()
devs = self.proxy.EnumerateDevices()
self.assertEqual(len(devs), 1)
mousebat0_up = devs[0]
self.assertEqual(
self.get_dbus_dev_property(mousebat0_up, "Model"), "Fancy Logitech mouse"
)
self.assertAlmostEqual(
self.get_dbus_dev_property(mousebat0_up, "Percentage"), 30.0
)
self.assertEqual(self.get_dbus_dev_property(mousebat0_up, "PowerSupply"), False)
self.assertEqual(
self.get_dbus_dev_property(mousebat0_up, "Type"), UP_DEVICE_KIND_MOUSE
)
self.assertEqual(self.get_dbus_dev_property(mousebat0_up, "Serial"), "654321")
self.assertEqual(self.get_dbus_property("OnBattery"), False)
self.assertEqual(
self.get_dbus_display_property("WarningLevel"), UP_DEVICE_LEVEL_NONE
)
self.stop_daemon()
def test_hidpp_receiver_mouse_with_keyboard_interface(self):
"""HID++ receiver mouse with mixed node stays classified as mouse"""
usb_parent = self.testbed.add_device(
"usb",
"/devices/pci0000:00/0000:00:01.2/0000:01:00.0/0000:02:08.0/0000:0d:00.1/usb1/1-5",
None,
[],
[],
)
intf = self.testbed.add_device(
"usb",
"/devices/pci0000:00/0000:00:01.2/0000:01:00.0/0000:02:08.0/0000:0d:00.1/usb1/1-5/1-5:1.2",
usb_parent,
[],
[],
)
receiver = self.testbed.add_device(
"hid",
"/devices/pci0000:00/0000:00:01.2/0000:01:00.0/0000:02:08.0/0000:0d:00.1/usb1/1-5/1-5:1.2/0003:046D:C53A.0004",
intf,
[],
[],
)
mouse = self.testbed.add_device(
"hid",
"/devices/pci0000:00/0000:00:01.2/0000:01:00.0/0000:02:08.0/0000:0d:00.1/usb1/1-5/1-5:1.2/0003:046D:C53A.0004/0003:046D:4099.0006",
receiver,
[],
[],
)
self.testbed.add_device(
"input",
"/devices/pci0000:00/0000:00:01.2/0000:01:00.0/0000:02:08.0/0000:0d:00.1/usb1/1-5/1-5:1.2/0003:046D:C53A.0004/0003:046D:4099.0006/input/input72",
mouse,
[],
[
"DEVNAME",
"input/event12",
"ID_INPUT_MOUSE",
"1",
"ID_INPUT_KEYBOARD",
"1",
],
)
self.testbed.add_device(
"input",
"/devices/pci0000:00/0000:00:01.2/0000:01:00.0/0000:02:08.0/0000:0d:00.1/usb1/1-5/1-5:1.2/0003:046D:C53A.0004/0003:046D:4099.0006/input/input73",
mouse,
[],
["DEVNAME", "input/event13", "ID_INPUT_KEYBOARD", "1"],
)
self.testbed.add_device(
"power_supply",
"/devices/pci0000:00/0000:00:01.2/0000:01:00.0/0000:02:08.0/0000:0d:00.1/usb1/1-5/1-5:1.2/0003:046D:C53A.0004/0003:046D:4099.0006/power_supply/hidpp_battery_0",
mouse,
[
"type",
"Battery",
"scope",
"Device",
"present",
"1",
"online",
"1",
"status",
"Discharging",
"capacity",
"69",
"model_name",
"Logitech G502 X PLUS",
],
[],
)
self.start_daemon()
devs = self.proxy.EnumerateDevices()
self.assertEqual(len(devs), 1)
mousebat0_up = devs[0]
self.assertEqual(
self.get_dbus_dev_property(mousebat0_up, "Model"), "Logitech G502 X PLUS"
)
self.assertAlmostEqual(
self.get_dbus_dev_property(mousebat0_up, "Percentage"), 69.0
)
self.assertEqual(self.get_dbus_dev_property(mousebat0_up, "PowerSupply"), False)
self.assertEqual(
self.get_dbus_dev_property(mousebat0_up, "Type"), UP_DEVICE_KIND_MOUSE
)
self.assertEqual(self.get_dbus_property("OnBattery"), False)
self.assertEqual(
self.get_dbus_display_property("WarningLevel"), UP_DEVICE_LEVEL_NONE
)
self.stop_daemon()
def test_hidpp_receiver_mouse_with_keyboard_interface_reverse_discovery(self):
"""HID++ receiver mixed mouse/keyboard remains mouse when keyboard appears first"""
usb_parent = self.testbed.add_device(
"usb",
"/devices/pci0000:00/0000:00:01.2/0000:01:00.0/0000:02:08.0/0000:0d:00.1/usb1/1-5",
None,
[],
[],
)
intf = self.testbed.add_device(
"usb",
"/devices/pci0000:00/0000:00:01.2/0000:01:00.0/0000:02:08.0/0000:0d:00.1/usb1/1-5/1-5:1.2",
usb_parent,
[],
[],
)
receiver = self.testbed.add_device(
"hid",
"/devices/pci0000:00/0000:00:01.2/0000:01:00.0/0000:02:08.0/0000:0d:00.1/usb1/1-5/1-5:1.2/0003:046D:C53A.0004",
intf,
[],
[],
)
mouse = self.testbed.add_device(
"hid",
"/devices/pci0000:00/0000:00:01.2/0000:01:00.0/0000:02:08.0/0000:0d:00.1/usb1/1-5/1-5:1.2/0003:046D:C53A.0004/0003:046D:4099.0006",
receiver,
[],
[],
)
self.testbed.add_device(
"input",
"/devices/pci0000:00/0000:00:01.2/0000:01:00.0/0000:02:08.0/0000:0d:00.1/usb1/1-5/1-5:1.2/0003:046D:C53A.0004/0003:046D:4099.0006/input/input73",
mouse,
[],
["DEVNAME", "input/event13", "ID_INPUT_KEYBOARD", "1"],
)
self.testbed.add_device(
"input",
"/devices/pci0000:00/0000:00:01.2/0000:01:00.0/0000:02:08.0/0000:0d:00.1/usb1/1-5/1-5:1.2/0003:046D:C53A.0004/0003:046D:4099.0006/input/input72",
mouse,
[],
[
"DEVNAME",
"input/event12",
"ID_INPUT_MOUSE",
"1",
"ID_INPUT_KEYBOARD",
"1",
],
)
self.testbed.add_device(
"power_supply",
"/devices/pci0000:00/0000:00:01.2/0000:01:00.0/0000:02:08.0/0000:0d:00.1/usb1/1-5/1-5:1.2/0003:046D:C53A.0004/0003:046D:4099.0006/power_supply/hidpp_battery_0",
mouse,
[
"type",
"Battery",
"scope",
"Device",
"present",
"1",
"online",
"1",
"status",
"Discharging",
"capacity",
"69",
"model_name",
"Logitech G502 X PLUS",
],
[],
)
self.start_daemon()
devs = self.proxy.EnumerateDevices()
self.assertEqual(len(devs), 1)
mousebat0_up = devs[0]
self.assertEqual(
self.get_dbus_dev_property(mousebat0_up, "Model"), "Logitech G502 X PLUS"
)
self.assertAlmostEqual(
self.get_dbus_dev_property(mousebat0_up, "Percentage"), 69.0
)
self.assertEqual(self.get_dbus_dev_property(mousebat0_up, "PowerSupply"), False)
self.assertEqual(
self.get_dbus_dev_property(mousebat0_up, "Type"), UP_DEVICE_KIND_MOUSE
)
self.assertEqual(self.get_dbus_property("OnBattery"), False)
self.assertEqual(
self.get_dbus_display_property("WarningLevel"), UP_DEVICE_LEVEL_NONE
)
self.stop_daemon()
def test_razer_mouse_with_keyboard_interface(self):
"""Razer mouse with mixed mouse/keyboard input stays classified as mouse"""
usb_parent = self.testbed.add_device(
"usb",
"/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2.2/1-2.2.2",
None,
[],
["DEVTYPE", "usb_device"],
)
intf0 = self.testbed.add_device(
"usb",
"/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2.2/1-2.2.2/1-2.2.2:1.0",
usb_parent,
[],
["DEVTYPE", "usb_interface"],
)
hid0 = self.testbed.add_device(
"hid",
"/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2.2/1-2.2.2/1-2.2.2:1.0/0003:1532:009C.003D",
intf0,
[],
["HID_NAME", "Razer Razer DeathAdder V2 X HyperSpeed"],
)
intf2 = self.testbed.add_device(
"usb",
"/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2.2/1-2.2.2/1-2.2.2:1.2",
usb_parent,
[],
["DEVTYPE", "usb_interface"],
)
hid2 = self.testbed.add_device(
"hid",
"/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2.2/1-2.2.2/1-2.2.2:1.2/0003:1532:009C.003E",
intf2,
[],
["HID_NAME", "Razer Razer DeathAdder V2 X HyperSpeed"],
)
self.testbed.add_device(
"input",
"/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2.2/1-2.2.2/1-2.2.2:1.0/0003:1532:009C.003D/input/input70",
hid0,
[],
[
"DEVNAME",
"input/event10",
"ID_INPUT_MOUSE",
"1",
"ID_INPUT_KEYBOARD",
"1",
],
)
self.testbed.add_device(
"input",
"/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2.2/1-2.2.2/1-2.2.2:1.2/0003:1532:009C.003E/input/input71",
hid2,
[],
["DEVNAME", "input/event11", "ID_INPUT_KEYBOARD", "1"],
)
self.testbed.add_device(
"power_supply",
"/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2.2/1-2.2.2/1-2.2.2:1.0/0003:1532:009C.003D/power_supply/razermouse_battery_0",
hid0,
[
"type",
"Battery",
"scope",
"Device",
"present",
"1",
"online",
"1",
"status",
"Discharging",
"capacity",
"69",
"model_name",
"Razer DeathAdder V2 X HyperSpeed",
],
[],
)
self.start_daemon()
devs = self.proxy.EnumerateDevices()
self.assertEqual(len(devs), 1)
razer_up = devs[0]
self.assertEqual(
self.get_dbus_dev_property(razer_up, "Model"),
"Razer DeathAdder V2 X HyperSpeed",
)
self.assertAlmostEqual(self.get_dbus_dev_property(razer_up, "Percentage"), 69.0)
self.assertEqual(self.get_dbus_dev_property(razer_up, "PowerSupply"), False)
self.assertEqual(
self.get_dbus_dev_property(razer_up, "Type"), UP_DEVICE_KIND_MOUSE
)
self.assertEqual(self.get_dbus_property("OnBattery"), False)
self.assertEqual(
self.get_dbus_display_property("WarningLevel"), UP_DEVICE_LEVEL_NONE
)
self.stop_daemon()
def test_razer_mouse_with_keyboard_interface_reverse_discovery(self):
"""Razer mouse remains a mouse when keyboard-only sibling appears first"""
usb_parent = self.testbed.add_device(
"usb",
"/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2.2/1-2.2.2",
None,
[],
["DEVTYPE", "usb_device"],
)
intf0 = self.testbed.add_device(
"usb",
"/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2.2/1-2.2.2/1-2.2.2:1.0",
usb_parent,
[],
["DEVTYPE", "usb_interface"],
)
hid0 = self.testbed.add_device(
"hid",
"/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2.2/1-2.2.2/1-2.2.2:1.0/0003:1532:009C.003D",
intf0,
[],
["HID_NAME", "Razer Razer DeathAdder V2 X HyperSpeed"],
)
intf2 = self.testbed.add_device(
"usb",
"/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2.2/1-2.2.2/1-2.2.2:1.2",
usb_parent,
[],
["DEVTYPE", "usb_interface"],
)
hid2 = self.testbed.add_device(
"hid",
"/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2.2/1-2.2.2/1-2.2.2:1.2/0003:1532:009C.003E",
intf2,
[],
["HID_NAME", "Razer Razer DeathAdder V2 X HyperSpeed"],
)
self.testbed.add_device(
"input",
"/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2.2/1-2.2.2/1-2.2.2:1.2/0003:1532:009C.003E/input/input71",
hid2,
[],
["DEVNAME", "input/event11", "ID_INPUT_KEYBOARD", "1"],
)
self.testbed.add_device(
"input",
"/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2.2/1-2.2.2/1-2.2.2:1.0/0003:1532:009C.003D/input/input70",
hid0,
[],
[
"DEVNAME",
"input/event10",
"ID_INPUT_MOUSE",
"1",
"ID_INPUT_KEYBOARD",
"1",
],
)
self.testbed.add_device(
"power_supply",
"/devices/pci0000:00/0000:00:14.0/usb1/1-2/1-2.2/1-2.2.2/1-2.2.2:1.0/0003:1532:009C.003D/power_supply/razermouse_battery_0",
hid0,
[
"type",
"Battery",
"scope",
"Device",
"present",
"1",
"online",
"1",
"status",
"Discharging",
"capacity",
"69",
"model_name",
"Razer DeathAdder V2 X HyperSpeed",
],
[],
)
self.start_daemon()
devs = self.proxy.EnumerateDevices()
self.assertEqual(len(devs), 1)
razer_up = devs[0]
self.assertEqual(
self.get_dbus_dev_property(razer_up, "Model"),
"Razer DeathAdder V2 X HyperSpeed",
)
self.assertAlmostEqual(self.get_dbus_dev_property(razer_up, "Percentage"), 69.0)
self.assertEqual(self.get_dbus_dev_property(razer_up, "PowerSupply"), False)
self.assertEqual(
self.get_dbus_dev_property(razer_up, "Type"), UP_DEVICE_KIND_MOUSE
)
self.assertEqual(self.get_dbus_property("OnBattery"), False)
self.assertEqual(
self.get_dbus_display_property("WarningLevel"), UP_DEVICE_LEVEL_NONE
)
self.stop_daemon()
def test_hidpp_two_mouses_unifying_receiver(self):
"""Upower shows wrong model name when a unifying receiver connects two mouses #309"""

View file

@ -335,6 +335,9 @@ up_device_supply_sibling_discovered_guess_type (UpDevice *device,
g_autoptr (GUdevDevice) parent_device = NULL;
g_autoptr (GUdevDevice) parent_sibling = NULL;
UpDeviceKind cur_type, new_type;
UpDeviceKind detected_type;
gboolean mixed_mouse_keyboard;
gboolean seen_mixed_mouse_keyboard;
gboolean is_same_parent = FALSE;
char *new_model_name;
char *model_name;
@ -388,6 +391,7 @@ up_device_supply_sibling_discovered_guess_type (UpDevice *device,
};
input = G_UDEV_DEVICE (sibling);
native_device = G_UDEV_DEVICE (up_device_get_native (device));
/* Do not process if we already have a "good" guess for the device type. */
g_object_get (device, "type", &cur_type, NULL);
@ -434,6 +438,12 @@ up_device_supply_sibling_discovered_guess_type (UpDevice *device,
break;
}
}
detected_type = new_type;
mixed_mouse_keyboard = detected_type == UP_DEVICE_KIND_MOUSE &&
g_udev_device_get_property_as_boolean (input, "ID_INPUT_KEYBOARD");
if (mixed_mouse_keyboard)
g_object_set_data (G_OBJECT (device), "up-seen-mixed-mouse-keyboard", GINT_TO_POINTER (TRUE));
seen_mixed_mouse_keyboard = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (device), "up-seen-mixed-mouse-keyboard"));
for (i = 0; i < G_N_ELEMENTS (priority); i++) {
if (priority[i] == cur_type || priority[i] == new_type) {
@ -442,6 +452,19 @@ up_device_supply_sibling_discovered_guess_type (UpDevice *device,
}
}
/* Some mice expose a mixed input node carrying both mouse and keyboard
* capabilities for extra buttons. Prefer mouse for that node and keep
* mouse stable if later siblings are keyboard-only. */
if (detected_type == UP_DEVICE_KIND_MOUSE &&
g_udev_device_get_property_as_boolean (input, "ID_INPUT_KEYBOARD") &&
(cur_type == UP_DEVICE_KIND_KEYBOARD || new_type == UP_DEVICE_KIND_KEYBOARD))
new_type = UP_DEVICE_KIND_MOUSE;
if (seen_mixed_mouse_keyboard &&
cur_type == UP_DEVICE_KIND_MOUSE &&
detected_type == UP_DEVICE_KIND_KEYBOARD)
new_type = UP_DEVICE_KIND_MOUSE;
/* Match audio sub-type */
if (new_type == UP_DEVICE_KIND_OTHER_AUDIO) {
const char *form_factor = g_udev_device_get_property (input, "SOUND_FORM_FACTOR");
@ -469,7 +492,6 @@ up_device_supply_sibling_discovered_guess_type (UpDevice *device,
}
if (cur_type != new_type) {
native_device = G_UDEV_DEVICE (up_device_get_native (device));
parent_device = g_udev_device_get_parent (native_device);
parent_sibling = g_udev_device_get_parent (input);