Repository: pwr-Solaar/Solaar

Issue:
G502 config section is duplicated every time Solaar is launched
**Information**
<!-- Make sure that your issue is not one of the known issues in the Solaar documentation at https://pwr-solaar.github.io/Solaar/ -->
<!-- Do not bother opening an issue for a version older than 1.1.0.  Upgrade to the latest version and see if your issue persists. -->
<!-- If you not running the current version of Solaar, strongly consider upgrading to the newest version. -->
- Solaar version (`solaar --version` or `git describe --tags` if cloned from this repository): `solaar 1.1.5`
- Distribution: `Arch`
- Kernel version (ex. `uname -srmo`): `Linux 5.19.9-zen1-1-zen x86_64 GNU/Linux`
- Output of `solaar show`:

<details>

```
Solaar version 1.1.5

USB and Bluetooth Devices

  1: G502 Gaming Mouse
     Device path  : /dev/hidraw1
     USB id       : 046d:C07D
     Codename     : G502
     Kind         : mouse
     Protocol     : HID++ 4.2
     Polling rate : 1 ms (1000Hz)
     Serial number:
     Model ID:      000000000000
     Unit ID:       00000000
          Firmware: U   88.02.B0017
        Bootloader: BOT 14.00.B0007
          Hardware: 72
             Other:
     Supports 20 HID++ 2.0 features:
         0: ROOT                   {0000} V0
         1: FEATURE SET            {0001} V0
         2: FEATURE INFO           {0002} V0
         3: DEVICE FW VERSION      {0003} V0
            Firmware: Firmware U   88.02.B0017
            Firmware: Bootloader BOT 14.00.B0007
            Firmware: Hardware  72
            Firmware: Other
            Unit ID: 00000000  Model ID: 000000000000  Transport IDs: {}
         4: DEVICE NAME            {0005} V0
            Name: Tunable FPS Gaming Mouse G502
            Kind: mouse
         5: LED CONTROL            {1300} V0
         6: unknown:18A1           {18A1} V0    internal, hidden
         7: unknown:1E00           {1E00} V0    hidden
         8: unknown:1E20           {1E20} V0
         9: unknown:1EB0           {1EB0} V0    internal, hidden
        10: ADJUSTABLE DPI         {2201} V0
            Sensitivity (DPI)        : 800
        11: ANGLE SNAPPING         {2230} V0
        12: SURFACE TUNING         {2240} V0
        13: REPORT RATE            {8060} V0
            Polling Rate (ms): 1
            Polling Rate (ms)        : 1
        14: ONBOARD PROFILES       {8100} V0
            Device Mode: Host
            Onboard Profiles        : Disable
        15: MOUSE BUTTON SPY       {8110} V0
        16: unknown:1850           {1850} V0    internal, hidden
        17: DFUCONTROL UNSIGNED    {00C1} V0
        18: unknown:1801           {1801} V0    internal, hidden
        19: DEVICE RESET           {1802} V0    internal, hidden
     Battery status unavailable.

  2: G733 Gaming Headset
     Device path  : /dev/hidraw2
     USB id       : 046d:0AB5
     Codename     : G733 Headset
     Kind         : headset
     Protocol     : HID++ 4.2
     Serial number:
     Model ID:      0AB500000000
     Unit ID:       FFFFFFFF
          Firmware: U1  37.00.B0131
     Supports 9 HID++ 2.0 features:
         0: ROOT                   {0000} V0
         1: FEATURE SET            {0001} V0
         2: DEVICE FW VERSION      {0003} V2
            Firmware: Firmware U1  37.00.B0131 0AB5
            Unit ID: FFFFFFFF  Model ID: 0AB500000000  Transport IDs: {'usbid': '0AB5'}
         3: DEVICE NAME            {0005} V0
            Name: G733 Gaming Headset
            Kind: None
         4: COLOR LED EFFECTS      {8070} V0
         5: GKEY                   {8010} V0
            Divert G Keys (saved): True
            Divert G Keys        : False
         6: EQUALIZER              {8310} V0
         7: SIDETONE               {8300} V0
            Sidetone (saved): 65
            Sidetone        : {'number': 255, 'request': 1805, 'error': 5, 'params': b''}
         8: ADC MEASUREMENT        {1F20} V0
            Battery status unavailable.
     Battery status unavailable.
```
</details>

- Contents of `~/.config/solaar/config.yaml` (or `~/.config/solaar/config.json` if `~/.config/solaar/config.yaml` not present):

<details>

```
- 1.1.5
- _NAME: G733 Gaming Headset
  _absent: [hi-res-scroll, lowres-scroll-mode, hires-smooth-invert, hires-smooth-resolution, hires-scroll-mode, smart-shift, thumb-scroll-invert, thumb-scroll-mode,
    onboard_profiles, report_rate, pointer_speed, dpi, speed-change, backlight, backlight-timed, fn-swap, reprogrammable-keys, persistent-remappable-keys,
    divert-keys, disable-keyboard-keys, crown-smooth, divert-crown, m-key-leds, mr-key-led, multiplatform, change-host, gesture2-gestures, gesture2-divert,
    gesture2-params, equalizer]
  _battery: 7968
  _modelId: 0AB500000000
  _sensitive: {divert-gkeys: true, sidetone: true}
  _unitId: FFFFFFFF
  divert-gkeys: true
  sidetone: 65
- _NAME: G502 Gaming Mouse
  _absent: [hi-res-scroll, lowres-scroll-mode, hires-smooth-invert, hires-smooth-resolution, hires-scroll-mode, smart-shift, thumb-scroll-invert, thumb-scroll-mode,
    pointer_speed, speed-change, backlight, backlight-timed, fn-swap, reprogrammable-keys, persistent-remappable-keys, divert-keys, disable-keyboard-keys,
    crown-smooth, divert-crown, divert-gkeys, m-key-leds, mr-key-led, multiplatform, change-host, gesture2-gestures, gesture2-divert, gesture2-params, sidetone,
    equalizer]
  _battery: 0
  _modelId: '000000000000'
  _unitId: '00000000'
  dpi: 800
  onboard_profiles: 2
  report_rate: 1
- _NAME: G502 Gaming Mouse
  _absent: [hi-res-scroll, lowres-scroll-mode, hires-smooth-invert, hires-smooth-resolution, hires-scroll-mode, smart-shift, thumb-scroll-invert, thumb-scroll-mode,
    pointer_speed, speed-change, backlight, backlight-timed, fn-swap, reprogrammable-keys, persistent-remappable-keys, divert-keys, disable-keyboard-keys,
    crown-smooth, divert-crown, divert-gkeys, m-key-leds, mr-key-led, multiplatform, change-host, gesture2-gestures, gesture2-divert, gesture2-params, sidetone,
    equalizer]
  _battery: 0
  _modelId: '000000000000'
  _unitId: '00000000'
  dpi: 800
  onboard_profiles: 2
  report_rate: 1
- _NAME: G502 Gaming Mouse
  _absent: [hi-res-scroll, lowres-scroll-mode, hires-smooth-invert, hires-smooth-resolution, hires-scroll-mode, smart-shift, thumb-scroll-invert, thumb-scroll-mode,
    pointer_speed, speed-change, backlight, backlight-timed, fn-swap, reprogrammable-keys, persistent-remappable-keys, divert-keys, disable-keyboard-keys,
    crown-smooth, divert-crown, divert-gkeys, m-key-leds, mr-key-led, multiplatform, change-host, gesture2-gestures, gesture2-divert, gesture2-params, sidetone,
    equalizer]
  _battery: 0
  _modelId: '000000000000'
  _unitId: '00000000'
  dpi: 800
  onboard_profiles: 2
  report_rate: 1
- _NAME: G502 Gaming Mouse
  _absent: [hi-res-scroll, lowres-scroll-mode, hires-smooth-invert, hires-smooth-resolution, hires-scroll-mode, smart-shift, thumb-scroll-invert, thumb-scroll-mode,
    pointer_speed, speed-change, backlight, backlight-timed, fn-swap, reprogrammable-keys, persistent-remappable-keys, divert-keys, disable-keyboard-keys,
    crown-smooth, divert-crown, divert-gkeys, m-key-leds, mr-key-led, multiplatform, change-host, gesture2-gestures, gesture2-divert, gesture2-params, sidetone,
    equalizer]
  _battery: 0
  _modelId: '000000000000'
  _unitId: '00000000'
  dpi: 800
  onboard_profiles: 2
  report_rate: 1
- _NAME: G502 Gaming Mouse
  _absent: [hi-res-scroll, lowres-scroll-mode, hires-smooth-invert, hires-smooth-resolution, hires-scroll-mode, smart-shift, thumb-scroll-invert, thumb-scroll-mode,
    pointer_speed, speed-change, backlight, backlight-timed, fn-swap, reprogrammable-keys, persistent-remappable-keys, divert-keys, disable-keyboard-keys,
    crown-smooth, divert-crown, divert-gkeys, m-key-leds, mr-key-led, multiplatform, change-host, gesture2-gestures, gesture2-divert, gesture2-params, sidetone,
    equalizer]
  _battery: 0
  _modelId: '000000000000'
  _unitId: '00000000'
  dpi: 800
  onboard_profiles: 2
  report_rate: 1
- _NAME: G502 Gaming Mouse
  _absent: [hi-res-scroll, lowres-scroll-mode, hires-smooth-invert, hires-smooth-resolution, hires-scroll-mode, smart-shift, thumb-scroll-invert, thumb-scroll-mode,
    pointer_speed, speed-change, backlight, backlight-timed, fn-swap, reprogrammable-keys, persistent-remappable-keys, divert-keys, disable-keyboard-keys,
    crown-smooth, divert-crown, divert-gkeys, m-key-leds, mr-key-led, multiplatform, change-host, gesture2-gestures, gesture2-divert, gesture2-params, sidetone,
    equalizer]
  _battery: 0
  _modelId: '000000000000'
  _unitId: '00000000'
  dpi: 800
  onboard_profiles: 2
  report_rate: 1
- _NAME: G502 Gaming Mouse
  _absent: [hi-res-scroll, lowres-scroll-mode, hires-smooth-invert, hires-smooth-resolution, hires-scroll-mode, smart-shift, thumb-scroll-invert, thumb-scroll-mode,
    pointer_speed, speed-change, backlight, backlight-timed, fn-swap, reprogrammable-keys, persistent-remappable-keys, divert-keys, disable-keyboard-keys,
    crown-smooth, divert-crown, divert-gkeys, m-key-leds, mr-key-led, multiplatform, change-host, gesture2-gestures, gesture2-divert, gesture2-params, sidetone,
    equalizer]
  _battery: 0
  _modelId: '000000000000'
  _unitId: '00000000'
  dpi: 800
  onboard_profiles: 2
  report_rate: 1
```

</details>


- Errors or warrnings from Solaar: `ERROR [MainThread] logitech_receiver.base: (18) device 255 error on feature request {080D}: 5 = logitech internal` <- there are a few other Errors like this one, but they all pertain to the device 255 (the headset)

<!-- Under normal operation Solaar keeps a log of warning and error messages in ~/.tmp
while it is running as a file starting with 'Solaar'.
If this file is not available or does not have useful information you can
run Solaar as `solaar -dd`, after killing any running Solaar processes to
have Solaar log informational, warning, and error messages to stdout. -->


**Describe the bug**
I have two devices recognized by Solaar: a G733 wireless headset, and a G502 wired mouse. Every time Solaar is launched, `~/.config/solaar/config.yaml` is appended to with a duplicate of the section for G502.

**To Reproduce**
Steps to reproduce the behavior:
1. Launch Solaar
2. Note the extra config section for G502



Discussion:
This happens because Solaar is assuming that devices have some identifying characteristics related to their model or unit ID, and that the model ID is non-zero.  This device has a zero model ID which makes Solaar treat it as anonymous and new each time it appears.  I'll think of a backup identifier to use.

Resolving patch:
diff --git a/lib/solaar/configuration.py b/lib/solaar/configuration.py
--- a/lib/solaar/configuration.py
+++ b/lib/solaar/configuration.py
@@ -160,15 +160,15 @@ def __setitem__(self, key, value):
         super().__setitem__(key, value)
         save(defer=True)
 
-    def update(self, device):
+    def update(self, device, modelId):
         if device.name and device.name != self.get(_KEY_NAME):
             super().__setitem__(_KEY_NAME, device.name)
         if device.wpid and device.wpid != self.get(_KEY_WPID):
             super().__setitem__(_KEY_WPID, device.wpid)
         if device.serial and device.serial != '?' and device.serial != self.get(_KEY_SERIAL):
             super().__setitem__(_KEY_SERIAL, device.serial)
-        if device.modelId and device.modelId != self.get(_KEY_MODEL_ID):
-            super().__setitem__(_KEY_MODEL_ID, device.modelId)
+        if modelId and modelId != self.get(_KEY_MODEL_ID):
+            super().__setitem__(_KEY_MODEL_ID, modelId)
         if device.unitId and device.unitId != self.get(_KEY_UNIT_ID):
             super().__setitem__(_KEY_UNIT_ID, device.unitId)
 
@@ -198,6 +198,7 @@ def named_int_representer(dumper, data):
 
 # A device can be identified by a combination of WPID and serial number (for receiver-connected devices)
 # or a combination of modelId and unitId (for direct-connected devices).
+# But some devices have empty (all zero) modelIds and unitIds.  Use the device name as a backup for the modelId.
 # The worst situation is a receiver-connected device that Solaar has never seen on-line
 # that is directly connected.  Here there is no way to realize that the two devices are the same.
 # So new entries are not created for unseen off-line receiver-connected devices except for those with protocol 1.0
@@ -211,8 +212,10 @@ def match(wpid, serial, modelId, unitId, c):
     if not _config:
         _load()
     entry = None
+    modelId = device.modelId if device.modelId != '000000000000' else device.name if device.modelId else None
+    print('PERSISTER for', device.name, device.wpid, device.serial, device.modelId, device.unitId, modelId)
     for c in _config:
-        if isinstance(c, _DeviceEntry) and match(device.wpid, device.serial, device.modelId, device.unitId, c):
+        if isinstance(c, _DeviceEntry) and match(device.wpid, device.serial, modelId, device.unitId, c):
             entry = c
             break
     if not entry:
@@ -220,9 +223,11 @@ def match(wpid, serial, modelId, unitId, c):
             if _log.isEnabledFor(_INFO):
                 _log.info('not setting up persister for offline device %s with missing serial number', device.name)
             return
+        if _log.isEnabledFor(_INFO):
+            _log.info('setting up persister for device %s', device.name)
         entry = _DeviceEntry()
         _config.append(entry)
-    entry.update(device)
+    entry.update(device, modelId)
     return entry
 
 


Test patch: