Fix crash writing per-key lighting from the CLI

PerKeyLighting.write_key_value dereferenced self._value without the
initialization guard that the base Settings.write_key_value has. On a
fresh CLI invocation (solaar config <dev> per-key-lighting <key> <color>)
nothing has read the setting yet, so _value is still None and the write
crashes with 'NoneType' object does not support item assignment.
The GUI never hits this because it reads all settings on attach.

Initialize via read(), which for this write-only 0x8081 setting loads
the persisted map or fabricates the all-"No change" sentinel map.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
grechmarlon 2026-06-11 18:22:25 +02:00
parent 3d1e502096
commit e844d0611d
2 changed files with 35 additions and 0 deletions

View File

@ -4101,6 +4101,8 @@ class PerKeyLighting(settings.Settings):
no_change = special_keys.COLORSPLUS["No change"]
zone_id = int(key)
if value != no_change:
if self._value is None:
self.read() # 0x8081 is write-only — read() loads persisted state or the sentinel map
self.update_key_value(zone_id, value, save)
if not self._device.online:
return value

View File

@ -672,3 +672,36 @@ def test_perkey_write_key_value_skipped_when_zone_is_animation(monkeypatch):
s.write_key_value(7, 0xFF0000)
s._send_zone_color.assert_not_called()
def test_perkey_write_key_value_initializes_unread_value(monkeypatch):
"""The CLI writes a single key without ever reading the setting, so
_value is still None. write_key_value must initialize it via read()
(persisted state or the sentinel map 0x8081 is write-only) instead
of crashing with a None dereference in update_key_value."""
from unittest.mock import MagicMock
from logitech_receiver import settings_templates
static_zone = _FakeZoneSetting("rgb_zone_1", _ValueWithID(0x01))
s = settings_templates.PerKeyLighting.__new__(settings_templates.PerKeyLighting)
device = MagicMock()
device.online = True
device.settings = [static_zone]
s._device = device
s._value = None # fresh CLI invocation: nothing has read the setting
s._validator = MagicMock()
s._validator.choices = [7]
s._pre_read = lambda cached=True: None
s._has_rgb_effects = True
s._send_with_retry = MagicMock(return_value=True)
s._send_zone_color = MagicMock(return_value=True)
s._fill_unset_zones_with_base_color = MagicMock(return_value=True)
monkeypatch.setattr(rgb_power, "translate_for_device", lambda d, c: c)
monkeypatch.setattr(rgb_power, "get_manager", lambda d: None)
s.write_key_value(7, 0xFF0000)
assert s._value[7] == 0xFF0000
s._send_zone_color.assert_called_once()