Repository: pwr-Solaar/Solaar

Issue:
CLI version not changing settings
**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.  Update to the latest version and see if your issue persists. -->
- Solaar version (`solaar --version` or `git describe --tags` if cloned from this repository): solaar 1.1.2
- Distribution: Linux Mint
- Kernel version (ex. `uname -srmo`): `Linux 5.4.0-107-generic x86_64 GNU/Linux`
- Output of `solaar show`:

<details>

```
~$ solaar show
Solaar version 1.1.2

Unifying Receiver
  Device path  : /dev/hidraw0
  USB id       : 046d:C52B
  Serial       : A4DE9A0E
    Firmware   : 24.11.B0036
    Bootloader : 02.09
    Other      : AA.AC
  Has 1 paired device(s) out of a maximum of 6.
  Notifications: wireless (0x000100)
  Device activity counters: 1=23

  1: Wireless Touch Keyboard K400 Plus
     Device path  : /dev/hidraw1
     WPID         : 404D
     Codename     : K400 Plus
     Kind         : keyboard
     Protocol     : HID++ 4.1
     Polling rate : 8 ms (125Hz)
     Serial number: 70CE825C
     Model ID:      404D00000000
     Unit ID:       563AF230
        Bootloader: BOT 22.02.B0002
          Firmware: RQK 63.02.B0016
             Other: 
     The power switch is located on the top edge.
     Supports 24 HID++ 2.0 features:
         0: ROOT                   {0000}   
         1: FEATURE SET            {0001}   
         2: DEVICE FW VERSION      {0003}   
            Firmware: Bootloader BOT 22.02.B0002 404D6217AD40
            Firmware: Firmware RQK 63.02.B0016 404D6217AD40
            Firmware: Other   
            Unit ID: 563AF230  Model ID: 404D00000000  Transport IDs: {'wpid': '404D'}
         3: DEVICE NAME            {0005}   
            Name: Wireless Touch Keyboard K400 Plus
            Kind: keyboard
         4: WIRELESS DEVICE STATUS {1D4B}   
         5: RESET                  {0020}   
         6: BATTERY STATUS         {1000}   
            Battery: 50%, discharging, next level 20%.
         7: REPROG CONTROLS V4     {1B04}   
            Key/Button Diversion (saved): {Volume Up:Regular, Show Desktop:Regular, Maximize Window:Regular, Switch Screen:Regular, Fn Left Click:Regular, Second Left Click:Regular, Volume Down:Regular, Mute:Regular, My Home:Regular, Play/Pause:Regular, Media Player:Regular, Next:Regular, Previous:Regular, Search:Regular, Sleep:Regular, Contextual Menu:Regular, Application Switcher:Regular, Back:Regular}
            Key/Button Diversion        : {Volume Up:Regular, Show Desktop:Regular, Maximize Window:Regular, Switch Screen:Regular, Fn Left Click:Regular, Second Left Click:Regular, Volume Down:Regular, Mute:Regular, My Home:Regular, Play/Pause:Regular, Media Player:Regular, Next:Regular, Previous:Regular, Search:Regular, Sleep:Regular, Contextual Menu:Regular, Application Switcher:Regular, Back:Regular}
         8: SWAP BUTTON CANCEL     {2005}   
         9: NEW FN INVERSION       {40A2}   
            Fn-swap: disabled
            Fn-swap default: enabled
            Swap Fx function (saved): False
            Swap Fx function        : False
        10: ENCRYPTION             {4100}   
        11: KEYBOARD DISABLE KEYS  {4521}   
            Disable keys (saved): {Caps Lock:False, Win:False, Scroll Lock:False, Insert:False}
            Disable keys        : {Caps Lock:False, Scroll Lock:False, Insert:False, Win:False}
        12: TOUCHPAD RAW XY        {6100}   
        13: GESTURE 2              {6501}   
            Gestures (saved): {'1': True, '10': True, '2': True, '30': True, '34': False, '42': True, '43': True, '45': False, '64': False, '65': False, '67': False, '84': True}
            Gestures        : {'1': True, '2': True, '30': True, '10': True, '45': False, '42': True, '43': True, '64': False, '65': False, '67': False, '84': True, '34': False}
            Gestures Diversion (saved): {'1': False, '10': False, '100': False, '2': False, '44': False, '64': False, '65': False, '67': False, '84': False, '85': False}
            Gestures Diversion        : {'1': False, '2': False, '10': False, '44': False, '64': False, '65': False, '67': False, '84': False, '85': False, '100': False}
            Gesture params (saved): {'4': {'scale': 256}}
            Gesture params        : {'4': {'scale': 256}}
        14: DFUCONTROL UNSIGNED    {00C1}   
        15: unknown:1811           {1811}   internal, hidden
        16: unknown:1830           {1830}   internal, hidden
        17: unknown:1890           {1890}   internal, hidden
        18: unknown:1DF3           {1DF3}   internal, hidden
        19: unknown:1E00           {1E00}   hidden
        20: unknown:1EB0           {1EB0}   internal, hidden
        21: unknown:1861           {1861}   internal, hidden
        22: unknown:18B0           {18B0}   internal, hidden
        23: unknown:1F11           {1F11}   internal, hidden
     Has 20 reprogrammable keys:
         0: Second Left Click         , default: Left Click                  => Left Click                
             mse, divertable, pos:0, group:0, group mask:empty
             reporting: default
         1: Back                      , default: Mouse Back Button           => Mouse Back Button         
             is FN, FN sensitive, reprogrammable, divertable, pos:1, group:0, group mask:empty
             reporting: default
         2: My Home                   , default: HomePage                    => HomePage                  
             is FN, FN sensitive, reprogrammable, divertable, pos:2, group:0, group mask:empty
             reporting: default
         3: Application Switcher      , default: Application Switcher        => Application Switcher      
             is FN, FN sensitive, reprogrammable, divertable, pos:3, group:0, group mask:empty
             reporting: default
         4: Contextual Menu           , default: Right Click                 => Right Click               
             is FN, FN sensitive, reprogrammable, divertable, pos:4, group:0, group mask:empty
             reporting: default
         5: Search                    , default: Search Files                => Search Files              
             is FN, FN sensitive, reprogrammable, divertable, pos:5, group:0, group mask:empty
             reporting: default
         6: Show Desktop              , default: Show Desktop                => Show Desktop              
             is FN, FN sensitive, reprogrammable, divertable, pos:6, group:0, group mask:empty
             reporting: default
         7: Maximize Window           , default: Maximize Window             => Maximize Window           
             is FN, FN sensitive, reprogrammable, divertable, pos:7, group:0, group mask:empty
             reporting: default
         8: Switch Screen             , default: Switch Presentation/Switch Screen => Switch Presentation/Switch Screen
             is FN, FN sensitive, reprogrammable, divertable, pos:8, group:0, group mask:empty
             reporting: default
         9: Media Player              , default: Music                       => Music                     
             is FN, FN sensitive, reprogrammable, divertable, pos:9, group:0, group mask:empty
             reporting: default
        10: Previous                  , default: Previous                    => Previous                  
             is FN, FN sensitive, divertable, pos:10, group:0, group mask:empty
             reporting: default
        11: Play/Pause                , default: Play/Pause                  => Play/Pause                
             is FN, FN sensitive, divertable, pos:11, group:0, group mask:empty
             reporting: default
        12: Next                      , default: Next                        => Next                      
             is FN, FN sensitive, divertable, pos:12, group:0, group mask:empty
             reporting: default
        13: Fn Left Click             , default: ShowUI                      => ShowUI                    
             divertable, pos:0, group:0, group mask:empty
             reporting: default
        14: Left Button               , default: Left Click                  => Left Click                
             mse, pos:0, group:0, group mask:empty
             reporting: default
        15: Right Button              , default: Right Click                 => Right Click               
             mse, pos:0, group:0, group mask:empty
             reporting: default
        16: Mute                      , default: Mute                        => Mute                      
             nonstandard, divertable, pos:0, group:0, group mask:empty
             reporting: default
        17: Volume Up                 , default: Volume Up                   => Volume Up                 
             nonstandard, divertable, pos:0, group:0, group mask:empty
             reporting: default
        18: Volume Down               , default: Volume Down                 => Volume Down               
             nonstandard, divertable, pos:0, group:0, group mask:empty
             reporting: default
        19: Sleep                     , default: Sleep                       => Sleep                     
             reprogrammable, divertable, pos:0, group:0, group mask:empty
             reporting: default
     Has 17 gesture(s), 1 param(s) and 5 spec(s):
        Track1Finger               Enabled(None): None   Diverted:(None) None
        TapGestures                Enabled(None): None   Diverted:(None) None
        Tap1Finger                 Enabled(   0): True   Diverted:(   0) False
        Tap2Finger                 Enabled(   1): True   Diverted:(   1) False
        TapDrag1Finger             Enabled(   2): True   Diverted:(None) None
        DoubleTap1Finger           Enabled(   3): True   Diverted:(   2) False
        Scroll2FingerStateless     Enabled(None): None   Diverted:(   3) False
        NaturalScrolling           Enabled(   4): False  Diverted:(None) None
        Scroll2FingerHoriz         Enabled(   5): True   Diverted:(None) None
        Scroll2FingerVert          Enabled(   6): True   Diverted:(None) None
        LeftEdgeSwipe1Finger2      Enabled(   7): False  Diverted:(   4) False
        RightEdgeSwipe1Finger2     Enabled(   8): False  Diverted:(   5) False
        TopEdgeSwipe1Finger2       Enabled(   9): False  Diverted:(   6) False
        Zoom2FingerStateless       Enabled(  10): True   Diverted:(   7) False
        FnClickGestureSuppression  Enabled(  11): False  Diverted:(None) None
        TwoFingersPresent          Enabled(None): None   Diverted:(   8) False
        DeviceSpecificRawData      Enabled(None): None   Diverted:(   9) False
        ScaleFactor                Value  (   0): 256 [Default: 256]
        period unit                Spec   (   3): 2048
        DVI field width            Spec   (   1): 8
        field widths               Spec   (   2): 8
        multiplier                 Spec   (   5): None
        resolution                 Spec   (   4): 4
     Battery: 50%, discharging, next level 20%.

```
</details>

**Describe the bug**
CLI version seemingly does nothing at all when changing settings and outputs errors when attempting to do so.
GUI version seemingly working fine, but I can't tolerate the ugly icon in my taskbar.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to 'terminal'
2. Change a setting 'solaar config 1 fn-swap 0'
3. Setting not changed and output as follows;
```
~$ solaar config 1 fn-swap 0
solaar: error: Traceback (most recent call last):
  File "/usr/share/solaar/lib/solaar/cli/__init__.py", line 205, in run
    m.run(c, args, _find_receiver, _find_device)
  File "/usr/share/solaar/lib/solaar/cli/config.py", line 174, in run
    setting = _settings_templates.check_feature_setting(dev, setting_name)
  File "/usr/share/solaar/lib/logitech_receiver/settings_templates.py", line 1227, in check_feature_setting
    feature = check_feature(device, setting)
  File "/usr/share/solaar/lib/logitech_receiver/settings_templates.py", line 1186, in check_feature
    if sclass.feature not in device.features:
  File "/usr/share/solaar/lib/logitech_receiver/hidpp20.py", line 316, in __contains__
    ivalue = int(featureId)
TypeError: int() argument must be a string, a bytes-like object or a number, not 'NoneType'
```

**Additional context**
First noticed the problem 2 days ago after a package update from stable ppa.
Currently on latest from git ppa but it's having the same issue.
No settings were changed before or after package update, (until unwanted behavior was noticed, of course).


Discussion:

Resolving patch:
diff --git a/lib/logitech_receiver/common.py b/lib/logitech_receiver/common.py
--- a/lib/logitech_receiver/common.py
+++ b/lib/logitech_receiver/common.py
@@ -119,12 +119,12 @@ def _readable_name(n):
         self._fallback = None
 
     @classmethod
-    def list(cls, items, name_generator=lambda x: str(x)):
+    def list(cls, items, name_generator=lambda x: str(x)):  # noqa: B008
         values = {name_generator(x): x for x in items}
         return NamedInts(**values)
 
     @classmethod
-    def range(cls, from_value, to_value, name_generator=lambda x: str(x), step=1):
+    def range(cls, from_value, to_value, name_generator=lambda x: str(x), step=1):  # noqa: B008
         values = {name_generator(x): x for x in range(from_value, to_value + 1, step)}
         return NamedInts(**values)
 
diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py
--- a/lib/logitech_receiver/settings_templates.py
+++ b/lib/logitech_receiver/settings_templates.py
@@ -1222,8 +1222,8 @@ def check_feature_settings(device, already_known):
 
 
 def check_feature_setting(device, setting_name):
-    for setting in SETTINGS:
-        if setting.name == setting_name:
-            feature = check_feature(device, setting)
-            if feature:
-                return feature
+    for sclass in SETTINGS:
+        if sclass.feature and sclass.name == setting_name and device.features:
+            setting = check_feature(device, sclass)
+            if setting:
+                return setting
diff --git a/lib/solaar/cli/config.py b/lib/solaar/cli/config.py
--- a/lib/solaar/cli/config.py
+++ b/lib/solaar/cli/config.py
@@ -172,9 +172,15 @@ def run(receivers, args, find_receiver, find_device):
 
     setting_name = args.setting.lower()
     setting = _settings_templates.check_feature_setting(dev, setting_name)
+    if not setting and dev.descriptor and dev.descriptor.settings:
+        for sclass in dev.descriptor.settings:
+            if sclass.register and sclass.name == setting_name:
+                try:
+                    setting = sclass.build(dev)
+                except Exception:
+                    setting = None
     if setting is None:
         raise Exception("no setting '%s' for %s" % (args.setting, dev.name))
-    _configuration.attach_to(dev)
 
     if args.value_key is None:
         setting.apply()
@@ -194,7 +200,7 @@ def run(receivers, args, find_receiver, find_device):
 
     # if the Solaar UI is running tell it to also perform the set
     if application.get_is_remote():
-        argl = ['config', dev.serial, setting.name]
+        argl = ['config', dev.serial or dev.unitId, setting.name]
         argl.extend([a for a in [args.value_key, args.extra_subkey, args.extra2] if a is not None])
         application.run(_yaml.safe_dump(argl))
 


Test patch: