Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions monitorcontrol/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
get_input_name,
Monitor,
PowerMode,
AudioMuteMode,
InputSource,
ColorPreset,
)
50 changes: 49 additions & 1 deletion monitorcontrol/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from . import get_monitors, get_input_name, PowerMode
from . import get_monitors, get_input_name, PowerMode, AudioMuteMode
from typing import List, Optional
import argparse
import importlib.metadata
Expand Down Expand Up @@ -30,6 +30,12 @@ def get_parser() -> argparse.ArgumentParser:
action="store_true",
help="Get the luminance of the first monitor.",
)
group.add_argument("--set-volume", type=int, help="Set the volume of all monitors.")
group.add_argument(
"--get-volume",
action="store_true",
help="Get the volume of the first monitor.",
)
group.add_argument(
"--get-power-mode",
action="store_true",
Expand All @@ -40,6 +46,16 @@ def get_parser() -> argparse.ArgumentParser:
choices=[mode.name for mode in PowerMode],
help="Set the power mode of all monitors.",
)
group.add_argument(
"--get-audio-mute-mode",
action="store_true",
help="Get the audio mute mode of the first monitor.",
)
group.add_argument(
"--set-audio-mute-mode",
choices=[mode.name for mode in AudioMuteMode],
help="Set the audio mute mode of all monitors.",
)
group.add_argument(
"--version", action="store_true", help="Show the version and exit."
)
Expand Down Expand Up @@ -109,12 +125,24 @@ def main(argv: Optional[List[str]] = None):
luminance = monitor_obj.get_luminance()
print(str(luminance))
return
elif args.get_volume:
monitor_obj = get_monitors()[monitor_index]
with monitor_obj:
volume = monitor_obj.get_volume()
sys.stdout.write(str(volume) + "\n")
return
elif args.get_power_mode:
monitor_obj = get_monitors()[monitor_index]
with monitor_obj:
power = monitor_obj.get_power_mode()
print(str(power.name))
return
elif args.get_audio_mute_mode:
monitor_obj = get_monitors()[monitor_index]
with monitor_obj:
audio_mute = monitor_obj.get_audio_mute_mode()
sys.stdout.write(str(audio_mute.name) + "\n")
return
elif args.set_luminance is not None:
if args.monitor is None:
for monitor_obj in get_monitors():
Expand All @@ -125,6 +153,16 @@ def main(argv: Optional[List[str]] = None):
with monitor_obj:
monitor_obj.set_luminance(args.set_luminance)
return
elif args.set_volume is not None:
if args.monitor is None:
for monitor_obj in get_monitors():
with monitor_obj:
monitor_obj.set_volume(args.set_volume)
else:
monitor_obj = get_monitors()[monitor_index]
with monitor_obj:
monitor_obj.set_volume(args.set_volume)
return
elif args.set_power_mode is not None:
if args.monitor is None:
for monitor_obj in get_monitors():
Expand All @@ -135,6 +173,16 @@ def main(argv: Optional[List[str]] = None):
with monitor_obj:
monitor_obj.set_power_mode(args.set_power_mode)
return
elif args.set_audio_mute_mode is not None:
if args.monitor is None:
for monitor_obj in get_monitors():
with monitor_obj:
monitor_obj.set_audio_mute_mode(args.set_audio_mute_mode)
else:
monitor_obj = get_monitors()[monitor_index]
with monitor_obj:
monitor_obj.set_audio_mute_mode(args.set_audio_mute_mode)
return
elif args.get_input_source:
monitor_obj = get_monitors()[monitor_index]
with monitor_obj:
Expand Down
112 changes: 112 additions & 0 deletions monitorcontrol/monitorcontrol.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ class PowerMode(enum.IntEnum):
off_hard = 0x05


@enum.unique
class AudioMuteMode(enum.Enum):
"""Monitor audio mute modes."""

#: On.
on = 0x01
#: Off.
off = 0x02


@enum.unique
class InputSource(enum.IntEnum):
"""Monitor input sources."""
Expand Down Expand Up @@ -243,6 +253,49 @@ def set_luminance(self, value: int):
"""
self._set_vcp_feature(vcp_codes.image_luminance, value)

def get_volume(self) -> int:
"""
Gets the monitors sound volume level.

Returns:
Current sound volume value.

Example:
Basic Usage::

from monitorcontrol import get_monitors

for monitor in get_monitors():
with monitor:
print(monitor.get_volume())

Raises:
VCPError: Failed to get sound volume from the VCP.
"""
return self._get_vcp_feature(vcp_codes.sound_volume)

def set_volume(self, value: int):
"""
Sets the monitors set_volume level.

Args:
value: New set_volume value (typically 0-100).

Example:
Basic Usage::

from monitorcontrol import get_monitors

for monitor in get_monitors():
with monitor:
monitor.set_volume(50)

Raises:
ValueError: Luminance outside of valid range.
VCPError: Failed to set sound volume in the VCP.
"""
self._set_vcp_feature(vcp_codes.sound_volume, value)

def get_color_preset(self) -> int:
"""
Gets the monitors color preset.
Expand Down Expand Up @@ -401,6 +454,65 @@ def set_power_mode(self, value: Union[int, str, PowerMode]):

self._set_vcp_feature(vcp_codes.display_power_mode, mode_value)

def get_audio_mute_mode(self) -> AudioMuteMode:
"""
Get the monitor audio mute mode.

Returns:
Value from the :py:class:`AudioMuteMode` enumeration.

Example:
Basic Usage::

from monitorcontrol import get_monitors

for monitor in get_monitors():
with monitor:
print(monitor.get_audio_mute_mode())

Raises:
VCPError: Failed to get the audio mute mode.
ValueError: Set audio mute state outside of valid range.
KeyError: Set audio mute mode string is invalid.
"""
value = self._get_vcp_feature(vcp_codes.display_audio_mute_mode)
return AudioMuteMode(value)

def set_audio_mute_mode(self, value: Union[int, str, AudioMuteMode]):
"""
Set the monitor audio mute mode.

Args:
value:
An integer audio mute mode,
or a string representing the audio mute mode,
or a value from :py:class:`AudioMuteMode`.

Example:
Basic Usage::

from monitorcontrol import get_monitors

for monitor in get_monitors():
with monitor:
monitor.set_audio_mute_mode("standby")

Raises:
VCPError: Failed to get or set the audio mute mode
ValueError: audio mute state outside of valid range.
AttributeError: audio mute mode string is invalid.
"""
if isinstance(value, str):
mode_value = getattr(AudioMuteMode, value).value
elif isinstance(value, int):
mode_value = AudioMuteMode(value).value
elif isinstance(value, AudioMuteMode):
mode_value = value.value
else:
raise TypeError("unsupported mode type: " + repr(type(value)))

self._set_vcp_feature(vcp_codes.display_audio_mute_mode, mode_value)

def get_input_source(self) -> int:
"""
Gets the monitors input source
Expand Down
12 changes: 12 additions & 0 deletions monitorcontrol/vcp/vcp_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ def writeable(self) -> bool:
code_type="rw",
function="c",
)
sound_volume = VCPCode(
name="sound volume",
value=0x62,
code_type="rw",
function="c",
)
image_contrast = VCPCode(
name="image contrast",
value=0x12,
Expand Down Expand Up @@ -98,3 +104,9 @@ def writeable(self) -> bool:
code_type="rw",
function="nc",
)
display_audio_mute_mode = VCPCode(
name="display audio mute mode",
value=0x8D,
code_type="rw",
function="nc",
)
32 changes: 32 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ def test_set_luminance(value: int):
api_mock.assert_called_once_with(value)


def test_get_volume():
with get_monitors_mock, mock.patch.object(Monitor, "get_volume") as api_mock:
main(["--get-volume"])
api_mock.assert_called_once()


@pytest.mark.parametrize("value", [0, 100])
def test_set_volume(value: int):
with get_monitors_mock, mock.patch.object(Monitor, "set_volume") as api_mock:
main(["--set-volume", str(value)])
api_mock.assert_called_once_with(value)


def test_get_power():
with get_monitors_mock, mock.patch.object(Monitor, "get_power_mode") as api_mock:
main(["--get-power"])
Expand All @@ -46,6 +59,25 @@ def test_set_power(value: int):
api_mock.assert_called_once_with(value)


def test_get_audio_mute():
with (
get_monitors_mock,
mock.patch.object(Monitor, "get_audio_mute_mode") as api_mock,
):
main(["--get-audio-mute"])
api_mock.assert_called_once()


@pytest.mark.parametrize("value", ["on", "off"])
def test_set_audio_mute(value: int):
with (
get_monitors_mock,
mock.patch.object(Monitor, "set_audio_mute_mode") as api_mock,
):
main(["--set-audio-mute", str(value)])
api_mock.assert_called_once_with(value)


def test_get_input_source():
with get_monitors_mock, mock.patch.object(Monitor, "get_input_source") as api_mock:
main(["--get-input-source"])
Expand Down
44 changes: 44 additions & 0 deletions tests/test_monitorcontrol.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ def get_test_vcps() -> List[Type[vcp.VCP]]:
else:
unit_test_vcp_dict = {
0x10: {"current": 50, "maximum": 100},
0x62: {"current": 50, "maximum": 100},
0xD6: {"current": 1, "maximum": 5},
0x8D: {"current": 1, "maximum": 2},
0x12: {"current": 50, "maximum": 100},
0x60: {"current": "HDMI1", "maximum": 3},
}
Expand Down Expand Up @@ -131,6 +133,22 @@ def test_luminance(
monitor.set_luminance(original)


@pytest.mark.parametrize(
"volume, expected", [(100, 100), (0, 0), (50, 50), (101, ValueError)]
)
def test_volume(monitor: Monitor, volume: int, expected: Union[int, Type[Exception]]):
original = monitor.get_volume()
try:
if isinstance(expected, int):
monitor.set_volume(volume)
assert monitor.get_volume() == expected
elif isinstance(expected, type(Exception)):
with pytest.raises(expected):
monitor.set_volume(volume)
finally:
monitor.set_volume(original)


@pytest.mark.skipif(USE_ATTACHED_MONITORS, reason="not going to change your contrast")
def test_contrast(monitor: Monitor):
contrast = monitor.get_contrast()
Expand Down Expand Up @@ -177,6 +195,32 @@ def test_power_mode(
monitor.set_power_mode(mode)


@pytest.mark.skipif(USE_ATTACHED_MONITORS, reason="not going to mute your monitors")
@pytest.mark.parametrize(
"mode, expected",
[
("on", 0x01),
(0x01, 0x01),
("INVALID", AttributeError),
(["on"], TypeError),
(0x00, ValueError),
(0x03, ValueError),
("off", 0x02),
],
)
def test_audio_mute_mode(
monitor: Monitor,
mode: Union[str, int],
expected: Union[int, Type[Exception]],
):
if isinstance(expected, (int, str)):
monitor.set_audio_mute_mode(mode)
assert monitor.get_audio_mute_mode().value == expected
elif isinstance(expected, type(Exception)):
with pytest.raises(expected):
monitor.set_audio_mute_mode(mode)


# ASUS VG27A when set to a mode that doesnt exist returned analog1 (0x1)
@pytest.mark.skipif(
USE_ATTACHED_MONITORS, reason="Real monitors dont support all input types"
Expand Down