Repository: newAM/monitorcontrol

Issue:
KeyError: 0 when running `monitorcontrol --get-monitors`
I have a setup with 8 total monitors on Windows 10, Python 3.8, monitorcontrol==3.0.2.  When I run:

`monitorcontrol --get-monitors`

I get:

```
Monitor 1: PA278QV
Available Inputs:
        InputSource.DVI1
        InputSource.DP1
        InputSource.DP2
        InputSource.HDMI1*
Monitor 2: P2422H
Available Inputs:
        InputSource.ANALOG1
        InputSource.DP1
        InputSource.HDMI1*
Monitor 3: P2422H
Available Inputs:
        InputSource.ANALOG1
        InputSource.DP1
        InputSource.HDMI1*
Monitor 4: PA278QV
Available Inputs:
        InputSource.DVI1
        InputSource.DP1
        InputSource.DP2
        InputSource.HDMI1*
Monitor 5: PA278QV
Available Inputs:
        InputSource.DVI1
        InputSource.DP1
        InputSource.DP2
        InputSource.HDMI1*
Monitor 6: P2419H
Available Inputs:
        InputSource.ANALOG1*
        InputSource.DP1
        InputSource.HDMI1
Monitor 7: P2419H
Available Inputs:
        InputSource.ANALOG1*
        InputSource.DP1
        InputSource.HDMI1
Traceback (most recent call last):
  File "c:\program files\python\python38\lib\runpy.py", line 193, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "c:\program files\python\python38\lib\runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "C:\Program Files\Python\Python38\Scripts\monitorcontrol.exe\__main__.py", line 7, in <module>
  File "c:\program files\python\python38\lib\site-packages\monitorcontrol\__main__.py", line 157, in main
    monitors_dict = monitor_obj.get_vcp_capabilities()
  File "c:\program files\python\python38\lib\site-packages\monitorcontrol\monitorcontrol.py", line 208, in get_vcp_capabilities
    res = _parse_capabilities(cap_str)
  File "c:\program files\python\python38\lib\site-packages\monitorcontrol\monitorcontrol.py", line 593, in _parse_capabilities
    caps_dict[key] = _convert_to_dict(_extract_a_cap(caps_str, key))
  File "c:\program files\python\python38\lib\site-packages\monitorcontrol\monitorcontrol.py", line 553, in _convert_to_dict
    result_dict[group].append(val)
KeyError: 0
```
It looks like it is failing when trying to retrieve information about monitor 8 (I assume, I'm just guessing). Monitor 8 is an HP monitor while all the other monitors are Dells and Asus monitors, in case that matters.

Any idea what to do here?

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa0a868..8b60b13 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,9 @@ as of version 2.1.1.
 - Added `get_color_preset` and `set_color_preset`.
 - Added a `description` attribute with the physical monitors description to `WindowsVCP`.
 
+### Fixed
+- Fixed parsing of nested capability groups.
+
 ## [3.0.3] - 2023-04-22
 ### Fixed
 - Fixed dangling file descriptors on linux platforms after calling `get_monitors()`.
diff --git a/monitorcontrol/monitorcontrol.py b/monitorcontrol/monitorcontrol.py
index b8156a6..d10a816 100644
--- a/monitorcontrol/monitorcontrol.py
+++ b/monitorcontrol/monitorcontrol.py
@@ -601,9 +601,9 @@ def _convert_to_dict(caps_str: str) -> dict:
         Expected string "04 14(05 06) 16" is converted to::
 
             {
-                0x04: [],
-                0x14: [0x05, 0x06],
-                0x16: [],
+                0x04: {},
+                0x14: {0x05: {}, 0x06: {}},
+                0x16: {},
             }
     """
 
@@ -613,22 +613,25 @@ def _convert_to_dict(caps_str: str) -> dict:
         return {}
 
     result_dict = {}
-    group = None
-    prev_digit = None
+    group = []
+    prev_val = None
     for chunk in caps_str.replace("(", " ( ").replace(")", " ) ").split(" "):
         if chunk == "":
             continue
         elif chunk == "(":
-            group = prev_digit
+            group.append(prev_val)
         elif chunk == ")":
-            group = None
+            group.pop(-1)
         else:
             val = int(chunk, 16)
-            if group is None:
-                result_dict[val] = []
+            if len(group) == 0:
+                result_dict[val] = {}
             else:
-                result_dict[group].append(val)
-            prev_digit = val
+                d = result_dict
+                for g in group:
+                    d = d[g]
+                d[val] = {}
+            prev_val = val
 
     return result_dict
 
@@ -677,7 +680,7 @@ def _parse_capabilities(caps_str: str) -> dict:
     input_source_cap = vcp.VCPCode("input_select").value
     if input_source_cap in caps_dict["vcp"]:
         caps_dict["inputs"] = []
-        input_val_list = caps_dict["vcp"][input_source_cap]
+        input_val_list = list(caps_dict["vcp"][input_source_cap].keys())
         input_val_list.sort()
 
         for val in input_val_list:
@@ -692,7 +695,7 @@ def _parse_capabilities(caps_str: str) -> dict:
     color_preset_cap = vcp.VCPCode("image_color_preset").value
     if color_preset_cap in caps_dict["vcp"]:
         caps_dict["color_presets"] = []
-        color_val_list = caps_dict["vcp"][color_preset_cap]
+        color_val_list = list(caps_dict["vcp"][color_preset_cap])
         color_val_list.sort()
 
         for val in color_val_list:


Test patch:
diff --git a/tests/test_monitorcontrol.py b/tests/test_monitorcontrol.py
index e65438a..8dba0c8 100644
--- a/tests/test_monitorcontrol.py
+++ b/tests/test_monitorcontrol.py
@@ -261,57 +261,64 @@ def test_convert_to_dict():
         "F7(00 01 02 03) F8(00 01) F9 EF FD(00 01) FE(00 01 02) FF"
     )
     expected = {
-        0x02: [],
-        0x04: [],
-        0x05: [],
-        0x08: [],
-        0x10: [],
-        0x12: [],
-        0x14: [0x05, 0x08, 0x0B],
-        0x16: [],
-        0x18: [],
-        0x1A: [],
-        0x52: [],
-        0x60: [0x11, 0x12, 0x0F, 0x10],
-        0xAC: [],
-        0xAE: [],
-        0xB2: [],
-        0xB6: [],
-        0xC0: [],
-        0xC6: [],
-        0xC8: [],
-        0xC9: [],
-        0xD6: [0x01, 0x04],
-        0xDF: [],
-        0x62: [],
-        0x8D: [],
-        0xF4: [],
-        0xF5: [0x00, 0x01, 0x02],
-        0xF6: [0x00, 0x01, 0x02],
-        0x4D: [],
-        0x4E: [],
-        0x4F: [],
-        0x15: [
-            0x01,
-            0x06,
-            0x09,
-            0x10,
-            0x11,
-            0x13,
-            0x14,
-            0x28,
-            0x29,
-            0x32,
-            0x44,
-            0x48,
-        ],
-        0xF7: [0x00, 0x01, 0x02, 0x03],
-        0xF8: [0x00, 0x01],
-        0xF9: [],
-        0xEF: [],
-        0xFD: [0x00, 0x01],
-        0xFE: [0x00, 0x01, 0x02],
-        0xFF: [],
+        0x02: {},
+        0x04: {},
+        0x05: {},
+        0x08: {},
+        0x10: {},
+        0x12: {},
+        0x14: {0x05: {}, 0x08: {}, 0x0B: {}},
+        0x16: {},
+        0x18: {},
+        0x1A: {},
+        0x52: {},
+        0x60: {0x11: {}, 0x12: {}, 0x0F: {}, 0x10: {}},
+        0xAC: {},
+        0xAE: {},
+        0xB2: {},
+        0xB6: {},
+        0xC0: {},
+        0xC6: {},
+        0xC8: {},
+        0xC9: {},
+        0xD6: {0x01: {}, 0x04: {}},
+        0xDF: {},
+        0x62: {},
+        0x8D: {},
+        0xF4: {},
+        0xF5: {0x00: {}, 0x01: {}, 0x02: {}},
+        0xF6: {0x00: {}, 0x01: {}, 0x02: {}},
+        0x4D: {},
+        0x4E: {},
+        0x4F: {},
+        0x15: {
+            0x01: {},
+            0x06: {},
+            0x09: {},
+            0x10: {},
+            0x11: {},
+            0x13: {},
+            0x14: {},
+            0x28: {},
+            0x29: {},
+            0x32: {},
+            0x44: {},
+            0x48: {},
+        },
+        0xF7: {0x00: {}, 0x01: {}, 0x02: {}, 0x03: {}},
+        0xF8: {0x00: {}, 0x01: {}},
+        0xF9: {},
+        0xEF: {},
+        0xFD: {0x00: {}, 0x01: {}},
+        0xFE: {0x00: {}, 0x01: {}, 0x02: {}},
+        0xFF: {},
     }
 
     assert _convert_to_dict(caps_str) == expected
+
+
+def test_convert_to_dict_nested():
+    # https://github.com/newAM/monitorcontrol/issues/249
+    caps_str = "DC(00(00 12 13 14))"
+    expected = {0xDC: {0: {0: {}, 0x12: {}, 0x13: {}, 0x14: {}}}}
+    assert _convert_to_dict(caps_str) == expected