Repository: pwr-Solaar/Solaar

Issue:
compute battery remaining percentage from voltage
**Information**
<!-- Please update to Solaar from this repository before asking for a new feature. -->
- Solaar version: `1.0.7rc2.r0.ge82457f`
- Distribution: Arch Linux
- Kernel version: Linux 5.14.14-arch1-1 x86_64 GNU/Linux
- Output of `solaar show` for the target device (if applicable):

<details>

```
➜ solaar show "G900"
  1: G900 Chaos Spectrum Gaming Mouse
     Device path  : /dev/hidraw16
     WPID         : 4053
     Codename     : G900
     Kind         : mouse
     Protocol     : HID++ 4.2
     Polling rate : 2 ms (500Hz)
     Serial number: 5BFEBB5F
     Model ID:      C08100000000
     Unit ID:       5BFEBB5F
          Firmware: MPM 01.05.B0023
        Bootloader: BOT 20.02.B0020
             Other:
     The power switch is located on the base.
     Supports 25 HID++ 2.0 features:
         0: ROOT                   {0000}
         1: FEATURE SET            {0001}
         2: DEVICE FW VERSION      {0003}
            Firmware: Firmware MPM 01.05.B0023 C081
            Firmware: Bootloader BOT 20.02.B0020 AABA
            Firmware: Other
            Unit ID: 5BFEBB5F  Model ID: C08100000000  Transport IDs: {'usbid': 'C081'}
         3: DEVICE NAME            {0005}
            Name: Gaming Mouse G900
            Kind: mouse
         4: BATTERY VOLTAGE        {1001}
            Battery: 4158mV, discharging, average.
         5: unknown:1863           {1863}   internal, hidden
         6: unknown:18A1           {18A1}   internal, hidden
         7: unknown:1E00           {1E00}   hidden
         8: unknown:1E20           {1E20}
         9: unknown:1EB0           {1EB0}   internal, hidden
        10: ADJUSTABLE DPI         {2201}
            Sensitivity (DPI) (saved): 1000
            Sensitivity (DPI)        : 1000
        11: ANGLE SNAPPING         {2230}
        12: SURFACE TUNING         {2240}
        13: REPORT RATE            {8060}
            Polling Rate (ms): 2
            Polling Rate (ms) (saved): 2
            Polling Rate (ms)        : 2
        14: ONBOARD PROFILES       {8100}
            Device Mode: Host
        15: MOUSE BUTTON SPY       {8110}
        16: unknown:1850           {1850}   internal, hidden
        17: DFUCONTROL SIGNED      {00C2}
        18: unknown:1801           {1801}   internal, hidden
        19: DEVICE RESET           {1802}   internal, hidden
        20: unknown:1890           {1890}   internal, hidden
        21: unknown:1811           {1811}   internal, hidden
        22: LATENCY MONITORING     {8111}
        23: COLOR LED EFFECTS      {8070}
        24: LED CONTROL            {1300}
     Battery: 4158mV, discharging, average.
```

```
➜ solaar show "G915 TKL"
  1: G915 TKL LIGHTSPEED Wireless RGB Mechanical Gaming Keyboard
     Device path  : None
     WPID         : 408E
     Codename     : G915 TKL
     Kind         : keyboard
     Protocol     : HID++ 4.2
     Polling rate : 1 ms (1000Hz)
     Serial number: E39C9BCF
     Model ID:      B35F408EC343
     Unit ID:       E39C9BCF
        Bootloader: BL1 12.00.B0017
             Other:
          Firmware: MPK 14.00.B0017
             Other:
             Other:
     The power switch is located on the top left corner.
     Supports 38 HID++ 2.0 features:
         0: ROOT                   {0000}
         1: FEATURE SET            {0001}
         2: DEVICE FW VERSION      {0003}
            Firmware: Bootloader BL1 12.00.B0017 00008B79E978
            Firmware: Other
            Firmware: Firmware MPK 14.00.B0017 408E8B79E978
            Firmware: Other
            Firmware: Other
            Unit ID: E39C9BCF  Model ID: B35F408EC343  Transport IDs: {'btleid': 'B35F', 'wpid': '408E', 'usbid': 'C343'}
         3: DEVICE NAME            {0005}
            Name: G915 TKL LIGHTSPEED Wireless RGB Mechanical Gaming Keyboard
            Kind: keyboard
         4: WIRELESS DEVICE STATUS {1D4B}
         5: RESET                  {0020}
         6: DEVICE FRIENDLY NAME   {0007}
            Friendly Name: G915 TKL
         7: BATTERY VOLTAGE        {1001}
            Battery: 4174mV, discharging, average.
         8: CHANGE HOST            {1814}
            Change Host        : 1:aura
         9: HOSTS INFO             {1815}
            Host 0 (paired): aura
            Host 1 (unpaired):
        10: RGB EFFECTS            {8071}
        11: PER KEY LIGHTING V2    {8081}
        12: REPROG CONTROLS V4     {1B04}
        13: REPORT HID USAGE       {1BC0}
        14: ENCRYPTION             {4100}
        15: KEYBOARD DISABLE BY USAGE {4522}
        16: KEYBOARD LAYOUT 2      {4540}
        17: GKEY                   {8010}
            Divert G Keys (saved): False
            Divert G Keys        : False
        18: MKEYS                  {8020}
        19: MR                     {8030}
        20: BRIGHTNESS CONTROL     {8040}
        21: ONBOARD PROFILES       {8100}
            Device Mode: On-Board
        22: REPORT RATE            {8060}
            Polling Rate (ms): 1
        23: DFUCONTROL SIGNED      {00C2}
        24: DFU                    {00D0}
        25: DEVICE RESET           {1802}   internal, hidden
        26: unknown:1803           {1803}   internal, hidden
        27: CONFIG DEVICE PROPS    {1806}   internal, hidden
        28: unknown:1813           {1813}   internal, hidden
        29: OOBSTATE               {1805}   internal, hidden
        30: unknown:1830           {1830}   internal, hidden
        31: unknown:1890           {1890}   internal, hidden
        32: unknown:1891           {1891}   internal, hidden
        33: unknown:18A1           {18A1}   internal, hidden
        34: unknown:1E00           {1E00}   hidden
        35: unknown:1EB0           {1EB0}   internal, hidden
        36: unknown:1861           {1861}   internal, hidden
        37: unknown:18B0           {18B0}   internal, hidden
     Has 2 reprogrammable keys:
         0: Host Switch Channel 1     , default: HostSwitch Channel 1        => HostSwitch Channel 1
             divertable, persistently divertable, pos:1, group:0, group mask:empty
             reporting: default
         1: Host Switch Channel 2     , default: HostSwitch Channel 2        => HostSwitch Channel 2
             divertable, persistently divertable, pos:2, group:0, group mask:empty
             reporting: default
     Battery: 4174mV, discharging, average.
```
</details>


**Is your feature request related to a problem? Please describe.**

I have a gaming mouse and keyboard from Logitech, G900 and G915 TKL respectively. Their battery level is reported in "mV", which doesn't really tell me anything about how long I should expect to be able to use the device before I have to charge it.

**Describe the solution you'd like**

I would love it if it was possible to display their estimated battery percentage instead.

**Describe alternatives you've considered**

I have read the README file, I've read the `--help` output and I've looked through the existing issues, and I haven't found any mention of "mV" 🤷

**Additional context**

Screenshot:

![image](https://user-images.githubusercontent.com/597206/138719920-ceb8c613-695d-4038-a2de-487c06d9fabd.png)



Discussion:
This requires a mapping from mV to percentage which requires some understanding of the battery.  It might be possible, but it's not in the current roadmap.
I have a table showing voltage to capacity for the battery in some Logitech device.  I'll put together a PR for this change.  Can you test it out on your devices to determine whether the mapping makes sense?

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
@@ -1170,6 +1170,25 @@ def get_voltage(device):
         return decipher_voltage(battery_voltage)
 
 
+# voltage to remaining charge from Logitech
+battery_voltage_remaining = (
+    (4186, 100),
+    (4067, 90),
+    (3989, 80),
+    (3922, 70),
+    (3859, 60),
+    (3811, 50),
+    (3778, 40),
+    (3751, 30),
+    (3717, 20),
+    (3671, 10),
+    (3646, 5),
+    (3579, 2),
+    (3500, 0),
+    (-1000, 0),
+)
+
+
 # modified to be much closer to battery reports
 def decipher_voltage(voltage_report):
     voltage, flags = _unpack('>HB', voltage_report[:3])
@@ -1194,6 +1213,11 @@ def decipher_voltage(voltage_report):
     elif (flags & (1 << 5)):
         charge_lvl = CHARGE_LEVEL.critical
 
+    for level in battery_voltage_remaining:
+        if level[0] < voltage:
+            charge_lvl = level[1]
+            break
+
     if _log.isEnabledFor(_DEBUG):
         _log.debug(
             'device ???, battery voltage %d mV, charging = %s, charge status %d = %s, charge level %s, charge type %s',
diff --git a/lib/solaar/ui/window.py b/lib/solaar/ui/window.py
--- a/lib/solaar/ui/window.py
+++ b/lib/solaar/ui/window.py
@@ -695,7 +695,7 @@ def _update_device_panel(device, panel, buttons, full=False):
 
         if battery_voltage is not None:
             panel._battery._label.set_text(_('Battery Voltage'))
-            text = '%(battery_voltage)dmV' % {'battery_voltage': battery_voltage}
+            text = '%(voltage)dmV, %(level)d%%' % {'voltage': battery_voltage, 'level': battery_level}
             tooltip_text = _('Voltage reported by battery')
         elif isinstance(battery_level, _NamedInt):
             panel._battery._label.set_text(_('Battery Level'))
@@ -909,7 +909,7 @@ def update_device(device, item, selected_device_id, need_popup, full=False):
         _model.set_value(item, _COLUMN.STATUS_TEXT, _CAN_SET_ROW_NONE)
         _model.set_value(item, _COLUMN.STATUS_ICON, _CAN_SET_ROW_NONE)
     else:
-        if battery_voltage is not None:
+        if battery_voltage is not None and False:  # Use levels instead of voltage here
             status_text = '%(battery_voltage)dmV' % {'battery_voltage': battery_voltage}
         elif isinstance(battery_level, _NamedInt):
             status_text = _(str(battery_level))


Test patch: