Repository: pwr-Solaar/Solaar

Issue:
Host name for linux machine paired with solaar shows up empty
**Information**
<!-- Do not bother opening an issue for a version older than 1.0.1.  Please 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.0.5`
- Distribution: Elementary OS 5 (Ubuntu 18.04)
- Kernel version (ex. `uname -srmo`): `Linux 4.20.17-042017-generic x86_64 GNU/Linux`
- Output of `solaar show`: 

<details>

```
Unifying Receiver
  Device path  : /dev/hidraw0
  USB id       : 046d:C52B
  Serial       : 05529561
    Firmware   : 12.11.B0032
    Bootloader : 04.16
    Other      : AA.AA
  Has 2 paired device(s) out of a maximum of 6.
  Notifications: wireless, software present (0x000900)
  Device activity counters: 1=39, 2=187

  1: Wireless Keyboard K375s
     Device path  : None
     WPID         : 4061
     Codename     : K375s
     Kind         : keyboard
     Protocol     : HID++ 4.5
     Polling rate : 20 ms (50Hz)
     Serial number: 06E177DF
     Model ID:      B34C40610000
     Unit ID:       58EFD8E0
        Bootloader: BOT 42.01.B0003
          Firmware: MPK 03.04.B0014
             Other: 
     The power switch is located on the base.
     Supports 31 HID++ 2.0 features:
         0: ROOT                   {0000}   
         1: FEATURE SET            {0001}   
         2: DEVICE FW VERSION      {0003}   
            Firmware: Bootloader BOT 42.01.B0003 0000B5877FBE01
            Firmware: Firmware MPK 03.04.B0014 4061B5877FBE01
            Firmware: Other   
            Unit ID: 58EFD8E0  Model ID: B34C40610000  Transport IDs: {'btleid': 'B34C', 'wpid': '4061'}
         3: DEVICE NAME            {0005}   
            Name: K370s/K375s Multi-Device Wireless Keyboard
            Kind: keyboard
         4: WIRELESS DEVICE STATUS {1D4B}   
         5: RESET                  {0020}   
         6: DEVICE FRIENDLY NAME   {0007}   
         7: BATTERY STATUS         {1000}   
            Battery: 90%, discharging, next level 50%.
         8: CHANGE HOST            {1814}   
            Change Host: 1:Swapnils-ASUS-VivoBook
         9: HOSTS INFO             {1815}   
            Host 0 (paired): 
            Host 1 (paired): Swapnil’s MacBook Air
            Host 2 (paired): Swapnil's Tab S6 Lite
        10: REPROG CONTROLS V4     {1B04}   
            Key/Button Diversion: {'209': 0, '210': 0, '211': 0, '213': 1, '214': 1, '189': 0, '6': 0, '4': 0, '5': 0, '3': 0, '2': 0, '1': 0, '191': 0, '212': 1, '10': 1, '221': 1, '193': 0, '192': 0}
        11: PERSISTENT REMAPPABLE ACTION {1C00}   
        12: K375S FN INVERSION     {40A3}   
            Swap Fx function: False
        13: ENCRYPTION             {4100}   
        14: LOCK KEY STATE         {4220}   
        15: KEYBOARD DISABLE KEYS  {4521}   
            Disable keys: {'1': False, '2': False, '4': False, '8': False, '16': False}
        16: MULTIPLATFORM          {4531}   
            Set OS: Windows
        17: DFUCONTROL SIGNED      {00C2}   
        18: unknown:1803           {1803}   internal, hidden
        19: CONFIG DEVICE PROPS    {1806}   internal, hidden
        20: OOBSTATE               {1805}   internal, hidden
        21: unknown:1813           {1813}   internal, hidden
        22: unknown:1830           {1830}   internal, hidden
        23: unknown:1861           {1861}   internal, hidden
        24: unknown:1890           {1890}   internal, hidden
        25: unknown:1891           {1891}   internal, hidden
        26: unknown:18A1           {18A1}   internal, hidden
        27: unknown:1DF3           {1DF3}   internal, hidden
        28: unknown:1E00           {1E00}   hidden
        29: unknown:1EB0           {1EB0}   internal, hidden
        30: unknown:18B0           {18B0}   internal, hidden
     Has 20 reprogrammable keys:
         0: Host Switch Channel 1     , default: HostSwitch Channel 1        => HostSwitch Channel 1      
             is FN, FN sensitive, divertable, persistently divertable, pos:1, group:0, group mask:empty
             reporting: default
         1: Host Switch Channel 2     , default: HostSwitch Channel 2        => HostSwitch Channel 2      
             is FN, FN sensitive, divertable, persistently divertable, pos:2, group:0, group mask:empty
             reporting: default
         2: Host Switch Channel 3     , default: HostSwitch Channel 3        => HostSwitch Channel 3      
             is FN, FN sensitive, divertable, persistently divertable, pos:3, group:0, group mask:empty
             reporting: default
         3: MultiPlatform Home/Mission Control, default: Multiplatform Home/Mission Control => Multiplatform Home/Mission Control
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, pos:4, group:0, group mask:empty
             reporting: diverted
         4: MultiPlatform Menu/Show/Hide Virtual Keyboard/Launchpad, default: Multiplatform Menu/Launchpad => Multiplatform Menu/Launchpad
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, pos:5, group:0, group mask:empty
             reporting: diverted
         5: Multiplatform Back        , default: MultiPlatform Back          => MultiPlatform Back        
             is FN, FN sensitive, reprogrammable, divertable, persistently divertable, pos:6, group:0, group mask:empty
             reporting: default
         6: Previous                  , default: Previous                    => Previous                  
             is FN, FN sensitive, divertable, persistently divertable, pos:7, group:0, group mask:empty
             reporting: default
         7: Play/Pause                , default: Play/Pause                  => Play/Pause                
             is FN, FN sensitive, divertable, persistently divertable, pos:8, group:0, group mask:empty
             reporting: default
         8: Next                      , default: Next                        => Next                      
             is FN, FN sensitive, divertable, persistently divertable, pos:9, group:0, group mask:empty
             reporting: default
         9: Mute                      , default: Mute                        => Mute                      
             is FN, FN sensitive, divertable, persistently divertable, pos:10, group:0, group mask:empty
             reporting: default
        10: Volume Down               , default: Volume Down                 => Volume Down               
             is FN, FN sensitive, divertable, persistently divertable, pos:11, group:0, group mask:empty
             reporting: default
        11: Volume Up                 , default: Volume Up                   => Volume Up                 
             is FN, FN sensitive, divertable, persistently divertable, pos:12, group:0, group mask:empty
             reporting: default
        12: Screen Capture/Print Screen, default: Screen Capture              => Screen Capture            
             is FN, reprogrammable, divertable, persistently divertable, pos:0, group:0, group mask:empty
             reporting: default
        13: MultiPlatform Search      , default: Multiplatform Search        => Multiplatform Search      
             is FN, reprogrammable, divertable, persistently divertable, pos:0, group:0, group mask:empty
             reporting: diverted
        14: Calculator                , default: Calculator                  => Calculator                
             is FN, reprogrammable, divertable, persistently divertable, pos:0, group:0, group mask:empty
             reporting: diverted
        15: Multi Platform Language Switch, default: Multiplatform Language Switch => Multiplatform Language Switch
             is FN, reprogrammable, divertable, persistently divertable, pos:0, group:0, group mask:empty
             reporting: diverted
        16: Fn Up                     , default: unknown:0070                => unknown:0070              
             is FN, divertable, persistently divertable, pos:0, group:0, group mask:empty
             reporting: default
        17: Fn Down                   , default: unknown:006F                => unknown:006F              
             is FN, divertable, persistently divertable, pos:0, group:0, group mask:empty
             reporting: default
        18: F Lock                    , default: Do Nothing One              => Do Nothing One            
             is FN, pos:0, group:0, group mask:empty
             reporting: default
        19: unknown:0034              , default: Do Nothing One              => Do Nothing One            
             nonstandard, pos:0, group:0, group mask:empty
             reporting: default
     Battery: 90%, discharging, next level 50%.

  2: Multi Device Silent Mouse M585/M590
     Device path  : None
     WPID         : 406B
     Codename     : M585/M590
     Kind         : mouse
     Protocol     : HID++ 4.5
     Polling rate : 8 ms (125Hz)
     Serial number: 95138309
     Model ID:      B01B406B0000
     Unit ID:       25F556B4
        Bootloader: BOT 48.01.B0002
          Firmware: MPM 05.10.B0011
             Other: 
     The power switch is located on the base.
     Supports 35 HID++ 2.0 features:
         0: ROOT                   {0000}   
         1: FEATURE SET            {0001}   
         2: DEVICE FW VERSION      {0003}   
            Firmware: Bootloader BOT 48.01.B0002 0000AE5E7277
            Firmware: Firmware MPM 05.10.B0011 406BAE5E7277
            Firmware: Other   
            Unit ID: 25F556B4  Model ID: B01B406B0000  Transport IDs: {'btleid': 'B01B', 'wpid': '406B'}
         3: DEVICE NAME            {0005}   
            Name: M585/M590 Multi-Device Mouse
            Kind: mouse
         4: WIRELESS DEVICE STATUS {1D4B}   
         5: RESET                  {0020}   
         6: CRYPTO ID              {0021}   
         7: DEVICE FRIENDLY NAME   {0007}   
         8: BATTERY STATUS         {1000}   
            Battery: 90%, discharging, next level 50%.
         9: CHANGE HOST            {1814}   
            Change Host: 1:Swapnil’s MacBook Air
        10: HOSTS INFO             {1815}   
            Host 0 (paired): Swapnil’s MacBook Air
            Host 1 (paired): Swapnil’s MacBook Air
        11: REPROG CONTROLS V4     {1B04}   
            Key/Button Actions: {'80': 80, '81': 81, '82': 82, '83': 83, '86': 86, '91': 83, '93': 86}
            Key/Button Diversion: {'82': 0, '83': 1, '86': 1, '91': 0, '93': 0, '215': 0}
        12: PERSISTENT REMAPPABLE ACTION {1C00}   
        13: POINTER SPEED          {2205}   
            Pointer Speed: 1.0
            Sensitivity (Pointer Speed): 256
        14: VERTICAL SCROLLING     {2100}   
            Roller type: standard
            Ratchet per turn: 18
            Scroll lines: 0
        15: DFUCONTROL SIGNED      {00C2}   
        16: DEVICE RESET           {1802}   internal, hidden
        17: unknown:1803           {1803}   internal, hidden
        18: CONFIG DEVICE PROPS    {1806}   internal, hidden
        19: OOBSTATE               {1805}   internal, hidden
        20: unknown:1813           {1813}   internal, hidden
        21: unknown:1830           {1830}   internal, hidden
        22: unknown:1861           {1861}   internal, hidden
        23: unknown:1890           {1890}   internal, hidden
        24: unknown:1891           {1891}   internal, hidden
        25: unknown:18A1           {18A1}   internal, hidden
        26: unknown:1DF3           {1DF3}   internal, hidden
        27: unknown:1E00           {1E00}   hidden
        28: unknown:1EB0           {1EB0}   internal, hidden
        29: unknown:18B1           {18B1}   internal, hidden
        30: unknown:1850           {1850}   internal, hidden
        31: unknown:1E22           {1E22}   
        32: unknown:1F03           {1F03}   internal, hidden
        33: unknown:18C0           {18C0}   internal, hidden
        34: LOWRES WHEEL           {2130}   
            Wheel Reports: HID
            Scroll Wheel HID++ Scrolling: False
     Has 8 reprogrammable keys:
         0: Left Button               , default: Left Click                  => Left Click                
             mse, reprogrammable, pos:0, group:1, group mask:g1
             reporting: default
         1: Right Button              , default: Right Click                 => Right Click               
             mse, reprogrammable, pos:0, group:1, group mask:g1
             reporting: default
         2: Middle Button             , default: Mouse Middle Button         => Mouse Middle Button       
             mse, reprogrammable, divertable, persistently divertable, raw XY, pos:0, group:2, group mask:g1,g2
             reporting: default
         3: Back Button               , default: Mouse Back Button           => Mouse Back Button         
             mse, reprogrammable, divertable, persistently divertable, raw XY, pos:0, group:2, group mask:g1,g2
             reporting: diverted
         4: Forward Button            , default: Mouse Forward Button        => Mouse Forward Button      
             mse, reprogrammable, divertable, persistently divertable, raw XY, pos:0, group:2, group mask:g1,g2
             reporting: diverted
         5: Left Tilt                 , default: Mouse Scroll Left Button    => Mouse Back Button         
             mse, reprogrammable, divertable, persistently divertable, raw XY, pos:0, group:2, group mask:g1,g2
             reporting: default
         6: Right Tilt                , default: Mouse Scroll Right Button   => Mouse Forward Button      
             mse, reprogrammable, divertable, persistently divertable, raw XY, pos:0, group:2, group mask:g1,g2
             reporting: default
         7: Virtual Gesture Button    , default: Virtual Gesture Button      => Virtual Gesture Button    
             divertable, virtual, raw XY, force raw XY, pos:0, group:3, group mask:empty
             reporting: default
     Battery: 90%, discharging, next level 50%.
```
</details>

**Describe the bug**
As is visible in the above `solaar show` output, the host name for my machine shows up as empty,
```
         9: HOSTS INFO             {1815}   
            Host 0 (paired): 
            Host 1 (paired): Swapnil’s MacBook Air
            Host 2 (paired): Swapnil's Tab S6 Lite
```
The other devices paired using bluetooth or Logitech Options on Mac show up with proper name.
In Logitech Options on the Mac, in Easy-Switch also the hostname for the linux machine shows up as empty.

I see that there is a `set_host_name` function in `hidpp20.py` but seems like it isn't used anywhere or during / after the pairing process.

**To Reproduce**
Steps to reproduce the behavior:
1. Run `solaar show`
2. Check HOSTS_INFO for any device paired using solaar
3. The hostname for the linux machine paired with solaar shows up as empty
4. Check the same on Logitech Options in Easy-Switch page and there also it shows up as empty



Discussion:
set_host_name was never used, and has a bug.

Solaar hasn't been set up to set the host name (but something is setting it on my laptops).   I'm going to add code to get_host_name to check whether the host name for the current connection does not match the current host name and set it if it doesn't.  This limits the number of times the host name is looked at and changed.


Couldn't the code to set the host name be called once right after pairing and never again ?
That would also mirror the behaviour of Logitech Options as it sets the host name and other properties (It even shows OS version for Mac OS) at the time of pairing and it is never changed again.
Also, why would adding the code to get_host_name limit the number of times the host name is looked at and changed ?
Changing only at pairing time doesn't work because the receiver can be moved between different computers, which should change the host name.  Also consider USB- and BT- connected devices.

One problem is when to check to see whether the host name is wrong.  That could be done at connection time, but it's simpler to do it when the host names are accessed.
That makes sense, thanks for the clarification and for the amazing work on Solaar :smile: 
Setting hostname in get_host_name should solve the issue of the hostname in `solaar show` being missing,
another behaviour I noticed with my K375s keyboard is as follows:
  -- New receiver plugged into linux laptop, opened Solaar and paired the keyboard
  -- Keyboard paired with the receiver in MacBook
  -- In Easy-Switch page of the keyboard in Logitech Options,
Slot 1 (paired to receiver in linux laptop) is empty,
while the one in Slot 2 shows Mac OS version and host name as `OS X <version> > <hostname>`
  -- Took the receiver in linux laptop and plugged into MacBook and then back to linux laptop.
  -- Now the blank name for Slot 1 is replaced with same info as Slot 2 `OS X <version> > <hostname>` and stays that way even after the receiver is plugged back into linux laptop and after opening Solaar.

Based on this looks like Logitech Options adds the `OS X <version> > <hostname>` name to the receiver when it is plugged in and detected by it but Solaar doesn't have the same behaviour, leading to blank name the first time a new receiver is plugged into linux laptop and then it doesn't update the info either when an existing receiver is plugged in, unlike Logitech options.

@pfps  Any idea on this behaviour and if this can be fixed, so that the proper names show up on Easy-Switch for linux machines ?
With the change I'm working on, the Unix hostname should show up.   Logitech might be doing something a bit different, but don't expect that behaviour to be emulated.  Also, Easy-Switch or even host name display for Logitech software may or may not work - it may depend on communication between the two instances of the Logitech software, which Solaar is not going to emulate.
Cool, once the change is in will test it and report back how the above behaviour is changed.

Resolving patch:
diff --git a/lib/logitech_receiver/hidpp20.py b/lib/logitech_receiver/hidpp20.py
--- a/lib/logitech_receiver/hidpp20.py
+++ b/lib/logitech_receiver/hidpp20.py
@@ -23,6 +23,7 @@
 
 from logging import DEBUG as _DEBUG
 from logging import ERROR as _ERROR
+from logging import INFO as _INFO
 from logging import WARNING as _WARNING
 from logging import getLogger
 from typing import List
@@ -1315,7 +1316,7 @@ def get_host_names(device):
         if capability_flags & 0x01:  # device can get host names
             for host in range(0, numHosts):
                 hostinfo = feature_request(device, FEATURE.HOSTS_INFO, 0x10, host)
-                _ignore, status, _ignore, numPages, nameLen, _ignore = _unpack('!BBBBBB', hostinfo[:6])
+                _ignore, status, _ignore, _ignore, nameLen, _ignore = _unpack('!BBBBBB', hostinfo[:6])
                 name = ''
                 remaining = nameLen
                 while remaining > 0:
@@ -1326,17 +1327,36 @@ def get_host_names(device):
                     else:
                         remaining = 0
                 host_names[host] = (bool(status), name)
+        # update the current host's name if it doesn't match the system name
+        import socket
+        hostname = socket.gethostname().partition('.')[0]
+        if host_names[currentHost][1] != hostname:
+            set_host_name(device, hostname, host_names[currentHost][1])
+            host_names[currentHost] = (host_names[currentHost][0], hostname)
     return host_names
 
 
-def set_host_name(device, name):
+def set_host_name(device, name, currentName=''):
+    name = bytearray(name, 'utf-8')
+    currentName = bytearray(currentName, 'utf-8')
+    if _log.isEnabledFor(_INFO):
+        _log.info('Setting host name to %s', name)
     state = feature_request(device, FEATURE.HOSTS_INFO, 0x00)
     if state:
-        flags = _unpack('!B', state[:1])[0]
+        flags, _ignore, _ignore, currentHost = _unpack('!BBBB', state[:4])
         if flags & 0x02:
-            hn = name[:min(14, name.find('.'))] if name.find('.') >= 0 else name
-            response = feature_request(device, FEATURE.HOSTS_INFO, 0x40, 0xff, 0, hn)
-            return response
+            hostinfo = feature_request(device, FEATURE.HOSTS_INFO, 0x10, currentHost)
+            _ignore, _ignore, _ignore, _ignore, _ignore, maxNameLen = _unpack('!BBBBBB', hostinfo[:6])
+            if name[:maxNameLen] == currentName[:maxNameLen] and False:
+                return True
+            length = min(maxNameLen, len(name))
+            chunk = 0
+            while chunk < length:
+                response = feature_request(device, FEATURE.HOSTS_INFO, 0x40, currentHost, chunk, name[chunk:chunk + 14])
+                if not response:
+                    return False
+                chunk += 14
+        return True
 
 
 def get_onboard_mode(device):


Test patch: