From badce9bb07d5d5183cd25289d95ff90af869642a Mon Sep 17 00:00:00 2001 From: Ken Sanislo Date: Tue, 19 May 2026 12:36:29 -0700 Subject: [PATCH] centurion: Headset icon kind at construction (#3226) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * device: seed Centurion device kind=headset at construction Centurion-transport devices have no static descriptor and are not receiver-paired, so their kind was only resolved by an online feature scan. A headset powered off when Solaar started showed no kind, hence no headphone icon in the tray/window — unlike receiver-paired mice and keyboards, whose kind comes from the receiver's persistent pairing registers. Every Centurion-transport device seen so far is a headset, and the centurion flag is known at construction time. Seed _kind=headset there so the icon is correct offline and on first run. Drop the now-dead online _infer_kind_centurion() feature scan and its Centurion branch in the kind property. * centurion: seed child headset kind via pairing_info The construction-time kind=headset seeding only covered the direct create_device path. G522-style devices reach a CenturionReceiver, whose notify_devices() builds the headset as a child Device with device_info=None and sets dev.centurion=True only after __init__ — so the __init__ guard never fired and the child kept kind=None. Set kind=headset in the pairing_info dict the receiver already passes into Device.__init__, which covers the bridge path. Both paths now seed the kind offline. --- lib/logitech_receiver/centurion.py | 3 +- lib/logitech_receiver/device.py | 23 ++++---------- tests/logitech_receiver/fake_hidpp.py | 1 - tests/logitech_receiver/test_device.py | 4 +++ .../logitech_receiver/test_hidpp20_complex.py | 30 +++++++++---------- 5 files changed, 26 insertions(+), 35 deletions(-) diff --git a/lib/logitech_receiver/centurion.py b/lib/logitech_receiver/centurion.py index 75fd2153..0056a5d2 100644 --- a/lib/logitech_receiver/centurion.py +++ b/lib/logitech_receiver/centurion.py @@ -35,6 +35,7 @@ from solaar import configuration from . import base from . import exceptions from . import hidpp10 +from . import hidpp10_constants from .centurion_constants import CenturionCoreFeature from .common import Alert from .common import Battery @@ -378,7 +379,7 @@ class CenturionReceiver: # Create child Device with receiver=self, number=1 pairing_info = { "wpid": self.product_id, - "kind": None, + "kind": hidpp10_constants.DEVICE_KIND.headset, # every Centurion-transport device so far is a headset "serial": None, "polling": None, "power_switch": None, diff --git a/lib/logitech_receiver/device.py b/lib/logitech_receiver/device.py index 1de933eb..28b67cab 100644 --- a/lib/logitech_receiver/device.py +++ b/lib/logitech_receiver/device.py @@ -146,6 +146,8 @@ class Device: self.status_callback = None # for changes to other potentially visible aspects self.wpid = pairing_info["wpid"] if pairing_info else None # the Wireless PID is unique per device model self._kind = pairing_info["kind"] if pairing_info else None # mouse, keyboard, etc (see hidpp10.DEVICE_KIND) + if self._kind is None and self.centurion: + self._kind = hidpp10_constants.DEVICE_KIND.headset # every Centurion-transport device so far is a headset self._serial = pairing_info["serial"] if pairing_info else None # serial number (an 8-char hex string) self._polling_rate = pairing_info["polling"] if pairing_info else None self._power_switch = pairing_info["power_switch"] if pairing_info else None @@ -322,27 +324,12 @@ class Device: @property def kind(self): + # Centurion devices are seeded with kind=headset at construction, so + # this online lookup only runs for descriptor-less HID++ 2.0 devices. if not self._kind and self.online and self.protocol >= 2.0: - if self.centurion: - self._kind = self._infer_kind_centurion() - else: - self._kind = _hidpp20.get_kind(self) + self._kind = _hidpp20.get_kind(self) return self._kind or "?" - def _infer_kind_centurion(self): - """Infer device kind from Centurion features (sub-device or top-level).""" - # Check sub-device features (wireless via bridge) - for feature in getattr(self, "_centurion_sub_features", ()): - if isinstance(feature, int) and 0x0600 <= feature <= 0x06FF: - return hidpp10_constants.DEVICE_KIND.headset - # Check top-level features (direct USB connection, no bridge) - if self.features: - for feature, _index in self.features.enumerate(): - feat_int = int(feature) if isinstance(feature, int) else 0 - if 0x0600 <= feat_int <= 0x06FF: - return hidpp10_constants.DEVICE_KIND.headset - return None - @property def firmware(self) -> tuple[common.FirmwareInfo]: if self._firmware is None and self.online: diff --git a/tests/logitech_receiver/fake_hidpp.py b/tests/logitech_receiver/fake_hidpp.py index 91fcabb8..3e4dc59b 100644 --- a/tests/logitech_receiver/fake_hidpp.py +++ b/tests/logitech_receiver/fake_hidpp.py @@ -403,7 +403,6 @@ class Device: gestures = device.Device.gestures __hash__ = device.Device.__hash__ feature_request = device.Device.feature_request - _infer_kind_centurion = device.Device._infer_kind_centurion def __post_init__(self): self._name = self.name diff --git a/tests/logitech_receiver/test_device.py b/tests/logitech_receiver/test_device.py index b291dba9..d7ae487f 100644 --- a/tests/logitech_receiver/test_device.py +++ b/tests/logitech_receiver/test_device.py @@ -108,6 +108,10 @@ def test_create_centurion_device(): assert test_device.hidpp_long is True assert int(test_device.handle) in base._centurion_handles + # kind is seeded at construction, so the headset icon shows even offline + test_device.online = False + assert test_device.kind == "headset" + # Clean up base._centurion_handles.discard(int(test_device.handle)) diff --git a/tests/logitech_receiver/test_hidpp20_complex.py b/tests/logitech_receiver/test_hidpp20_complex.py index 8fb6e3ca..1e754f37 100644 --- a/tests/logitech_receiver/test_hidpp20_complex.py +++ b/tests/logitech_receiver/test_hidpp20_complex.py @@ -1116,21 +1116,6 @@ def test_centurion_sub_device_hardware_info(): 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 --- @@ -1228,6 +1213,21 @@ def test_centurion_receiver_container_with_device(): recv[2] +def test_centurion_receiver_child_device_kind_headset(): + """notify_devices() seeds the headset child with kind=headset via pairing_info, + so the headphone icon shows even when the headset is powered off at startup.""" + info = FakeCenturionDeviceInfo(product="PRO X 2 LIGHTSPEED") + recv = CenturionReceiver(FakeLowLevel(), 0x99, info) + recv._pending = False + recv._dongle_features = [] + + recv.notify_devices() + + child = recv[1] + child.online = False + assert child.kind == "headset" + + def test_centurion_receiver_enable_connection_notifications(): """CenturionReceiver.enable_connection_notifications() returns False.""" info = FakeCenturionDeviceInfo()