Leave lighting alone on connect when LED control is off

Connect-time apply pushed persisted zone effects and the LED-control
release write even when the user had LED control switched off — with
the G560 (0x8070) accepting zone writes without a SW-control claim,
Solaar repainted lighting it didn't own on every startup, stomping the
device's onboard effect or any other controller (e.g. OpenRGB).

Off now doubles as "leave the lighting alone": LEDControl.apply skips
the wire entirely unless the persisted value is an explicit claim, and
zone-effect applies follow their control gate (led_control for 0x8070,
rgb_control for 0x8071 — the same gate PerKeyLighting already
enforces).  When the gate is on, the claim itself repaints the saved
zone effects, matching the RGBControl claim flow.
This commit is contained in:
Ken Sanislo 2026-07-16 21:08:21 -07:00
parent bdc17e9fb5
commit 3b76be9abb
2 changed files with 73 additions and 0 deletions

View File

@ -3246,6 +3246,18 @@ class LEDControl(settings.Setting):
logger.warning("%s: post-claim repaint of %s failed: %s", self._device, s.name, e)
return result
def apply(self):
# Off = leave the lighting alone on connect (don't send the release);
# another app may own it. Only an explicit On claim is applied.
try:
value = self.read(self.persist)
except Exception:
value = None
if value:
super().apply()
elif logger.isEnabledFor(logging.DEBUG):
logger.debug("%s: LED control off — leaving lighting untouched on %s", self.name, self._device)
colors = special_keys.COLORS
_LEDP = hidpp20.LEDParam
@ -3257,6 +3269,20 @@ class LEDZoneSetting(settings.Setting):
label = _("LED Zone Effects")
description = _("Set effect for LED Zone") + "\n" + _("LED Control needs to be enabled.")
feature = _F.COLOR_LED_EFFECTS
gate_setting_name = LEDControl.name
def apply(self):
# Skip when the control gate is off (SETTINGS order runs it first, so
# its _value is set) — leave the lighting to whatever owns it.
for s in self._device.settings:
if s.name == self.gate_setting_name:
if not s._value:
if logger.isEnabledFor(logging.DEBUG):
logger.debug("%s: %s off — not applying on %s", self.name, s.name, self._device)
return
break
super().apply()
color_field = {"name": _LEDP.color, "kind": settings.Kind.COLOR, "label": _("Color")}
speed_field = {"name": _LEDP.speed, "kind": settings.Kind.RANGE, "label": _("Speed"), "min": 0, "max": 255}
period_field = {
@ -3809,6 +3835,7 @@ class RGBEffectSetting(LEDZoneSetting):
label = _("LED Zone Effects")
description = _("Set effect for LED Zone") + "\n" + _("LED Control needs to be enabled.")
feature = _F.RGB_EFFECTS
gate_setting_name = RGBControl.name
# 0x8071 firmware-fixes ramp/form bytes; drop those widgets here.
possible_fields = [
LEDZoneSetting.color_field,

View File

@ -1072,3 +1072,49 @@ def test_HeadsetOnboardEffect_absent_animated_fields_seed_defaults():
assert effect.intensity == 100
assert effect.period == 5000
def _led_lighting_device():
responses = [
fake_hidpp.Response("0100000001", 0x0400),
fake_hidpp.Response("00000102", 0x0410, "00FF00"),
fake_hidpp.Response("0000000300040005", 0x0420, "000000"),
fake_hidpp.Response("0001000B00080009", 0x0420, "000100"),
fake_hidpp.Response("01", 0x0480, "01"),
fake_hidpp.Response("000000000000000020500000", 0x0430, "000000000000000020500000"),
]
device = fake_hidpp.Device(responses=responses, feature=hidpp20_constants.SupportedFeature.COLOR_LED_EFFECTS, offset=4)
control = settings_templates.check_feature(device, settings_templates.LEDControl)
zones = settings_templates.check_feature(device, settings_templates.LEDZoneSetting)
device.settings = [control] + zones
return device
def test_lighting_apply_led_control_off_leaves_lighting_alone(mocker):
"""With led_control persisted off, connect-time apply must not send any
lighting traffic the device or another app (e.g. OpenRGB) owns it."""
device = _led_lighting_device()
device.persister["led_control"] = False
device.persister[device.settings[1].name] = hidpp20.LEDEffectSetting(ID=3, period=0x20, intensity=0x50)
spy = mocker.spy(device, "request")
for s in device.settings:
s.apply()
assert spy.call_count == 0
def test_lighting_apply_led_control_on_claims_and_repaints(mocker):
"""With led_control persisted on, apply claims SW control and pushes the
saved zone effect."""
device = _led_lighting_device()
device.persister["led_control"] = True
device.persister[device.settings[1].name] = hidpp20.LEDEffectSetting(ID=3, period=0x20, intensity=0x50)
spy = mocker.spy(device, "request")
for s in device.settings:
s.apply()
request_ids = [c.args[0] for c in spy.call_args_list]
assert 0x0480 in request_ids # SetSWControl claim
assert 0x0430 in request_ids # zone effect write