From 1c43a934ac3322d9f6d590f9b1ee9c25666853ab Mon Sep 17 00:00:00 2001 From: Guillaume Date: Fri, 9 Oct 2026 22:30:20 +0200 Subject: [PATCH] fix: handle missing parentheses in capabilities strings --- monitorcontrol/monitorcontrol.py | 12 +++++ tests/test_monitorcontrol.py | 75 ++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/monitorcontrol/monitorcontrol.py b/monitorcontrol/monitorcontrol.py index 8166a76..35f5c21 100644 --- a/monitorcontrol/monitorcontrol.py +++ b/monitorcontrol/monitorcontrol.py @@ -2,6 +2,7 @@ from types import TracebackType from typing import List, Optional, Type, Union import enum +import re import sys @@ -650,8 +651,19 @@ def _extract_a_cap(caps_str: str, key: str) -> str: start_of_filter += len(key) filtered_caps_str = caps_str[start_of_filter:] + named_fields = set() + if key.lower() in ("cmds", "vcp"): + named_fields = { + match.start() + for match in re.finditer(r"\b([A-Za-z_][A-Za-z0-9_]*)\(", filtered_caps_str) + if not re.fullmatch(r"[A-Fa-f0-9]+", match.group(1)) + } + end_of_filter = 0 for i in range(len(filtered_caps_str)): + if end_of_filter == 1 and i in named_fields: + # Some monitors omit the closing parenthesis before the next field. + break if filtered_caps_str[i] == "(": end_of_filter += 1 if filtered_caps_str[i] == ")": diff --git a/tests/test_monitorcontrol.py b/tests/test_monitorcontrol.py index 32fb698..57f5ab6 100644 --- a/tests/test_monitorcontrol.py +++ b/tests/test_monitorcontrol.py @@ -1,11 +1,13 @@ from monitorcontrol import vcp, vcp_codes from monitorcontrol.monitorcontrol import ( + ColorPreset, InputSource, get_monitors, get_input_name, get_vcps, Monitor, _convert_to_dict, + _extract_a_cap, ) from types import TracebackType from typing import Iterable, List, Optional, Tuple, Type, Union @@ -299,6 +301,79 @@ def test_get_vcp_capabilities(monitor: Monitor): ] +@pytest.mark.parametrize("vcp_closing", ["", ")"]) +def test_get_vcp_capabilities_lemon25(vcp_closing): + caps_str = ( + "(prot(monitor)type(lcd)model(LEMON25)cmds(01 02 03 07 0C E3 F3)" + "vcp(02 04 05 08 10 12 14(05 08 0B 0C) 16 18 1A 52 " + "60(11 12 0F ) 62 87 8D CC" + f"{vcp_closing} mccs_ver(2.2)mswhql(1))" + ) + test_vcp = UnitTestVCP({}) + with mock.patch.object(test_vcp, "get_vcp_capabilities", return_value=caps_str): + with Monitor(test_vcp) as monitor: + caps = monitor.get_vcp_capabilities() + + assert caps["prot"] == "monitor" + assert caps["type"] == "lcd" + assert caps["model"] == "LEMON25" + assert caps["cmds"] == { + 0x01: {}, + 0x02: {}, + 0x03: {}, + 0x07: {}, + 0x0C: {}, + 0xE3: {}, + 0xF3: {}, + } + assert caps["vcp"] == { + 0x02: {}, + 0x04: {}, + 0x05: {}, + 0x08: {}, + 0x10: {}, + 0x12: {}, + 0x14: {0x05: {}, 0x08: {}, 0x0B: {}, 0x0C: {}}, + 0x16: {}, + 0x18: {}, + 0x1A: {}, + 0x52: {}, + 0x60: {0x11: {}, 0x12: {}, 0x0F: {}}, + 0x62: {}, + 0x87: {}, + 0x8D: {}, + 0xCC: {}, + } + assert caps["inputs"] == [InputSource.DP1, InputSource.HDMI1, InputSource.HDMI2] + assert caps["color_presets"] == [ + ColorPreset.COLOR_TEMP_6500K, + ColorPreset.COLOR_TEMP_9300K, + ColorPreset.COLOR_TEMP_USER1, + ColorPreset.COLOR_TEMP_USER2, + ] + assert caps["mccs_ver"] == "2.2" + assert caps["mswhql"] == "1" + + +@pytest.mark.parametrize("key", ["cmds", "vcp"]) +@pytest.mark.parametrize("closing", ["", ")"]) +@pytest.mark.parametrize("next_field", ["mccs_ver(2.2)", "MSWHQL(1)"]) +def test_extract_numeric_cap_next_field(key, closing, next_field): + values = "DC(00(00 12 13 14)) CC" + caps_str = f"({key}({values}{closing} {next_field})" + extracted = _extract_a_cap(caps_str, key) + assert extracted.strip() == values + assert _convert_to_dict(extracted) == { + 0xDC: {0: {0: {}, 0x12: {}, 0x13: {}, 0x14: {}}}, + 0xCC: {}, + } + + +def test_convert_to_dict_invalid_token(): + with pytest.raises(ValueError): + _convert_to_dict("10 invalid 12") + + def test_convert_to_dict(): # https://github.com/newAM/monitorcontrol/issues/110 caps_str = (