Enable GUI unpair for Lightspeed receivers

Flip _lightspeed_receiver() to may_unpair: True so the GUI unpair button
becomes sensitive for Lightspeed-paired devices, and route the GUI unpair
action through _unpair_device(n, force=True) so the unpair register write
actually fires instead of short-circuiting to cache invalidation.

The previous GUI path called `del receiver[n]`, which dispatches to
Receiver.__delitem__ → _unpair_device(n, force=False). On receivers with
re_pairs=True (Lightspeed, Nano) that hits the cache-invalidation branch
at receiver.py:391 and never writes the unpair register — a "fake unpair"
that would have left the slot bound on the hardware even after the button
was enabled.

With force=True, the GUI now issues RECEIVER_PAIRING action 0x03 for the
selected slot, matching the CLI unpair path (cli/unpair.py:39) which has
always used force=True. Lightspeed and Unifying unpair behavior are now
symmetric: the button is enabled, the confirmation dialog is shown, and
the register write is performed.

The pair/add flow is untouched: it still uses set_lock(device=0) which
lets the receiver firmware pick an empty slot, re_pairs remains True so
the listener's silent-replace branch continues to handle re-pair into an
occupied devnumber. Verified on dual-slot C547 hardware that pairing into
an empty slot preserves the occupant of the other slot.

Stale pairings where Solaar can't enumerate the slot (no cached device
row to right-click) still require the --slot CLI from the preceding
commit — that path is orthogonal to this GUI enablement.
This commit is contained in:
Ken Sanislo 2026-04-12 13:57:21 -07:00
parent 0a31711b3a
commit a6709d0ee4
2 changed files with 6 additions and 2 deletions

View File

@ -123,7 +123,7 @@ def _lightspeed_receiver(product_id: int) -> dict:
"usb_interface": 2,
"receiver_kind": "lightspeed",
"name": _("Lightspeed Receiver"),
"may_unpair": False,
"may_unpair": True,
"re_pairs": True,
}

View File

@ -98,6 +98,10 @@ def unpair(window, device):
device_number = device.number
try:
del receiver[device_number]
# force=True ensures the unpair register write is issued even on
# re_pairs receivers (Lightspeed, Nano); otherwise _unpair_device
# short-circuits to cache-invalidation only and the slot stays
# bound on the hardware.
receiver._unpair_device(device_number, True)
except Exception:
common.error_dialog(common.ErrorReason.UNPAIR, device)