From b6f9cf674949a151923877279d53bc9521a78bde Mon Sep 17 00:00:00 2001 From: SeoFood Date: Sun, 11 Oct 2026 02:38:25 +0200 Subject: [PATCH] atvscript: use Audio and Power for volume and power commands --- docs/documentation/atvscript.md | 5 +++++ pyatv/scripts/atvscript.py | 8 ++++++++ tests/scripts/test_atvscript.py | 36 +++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/docs/documentation/atvscript.md b/docs/documentation/atvscript.md index e7f4129ff..e9a15039d 100644 --- a/docs/documentation/atvscript.md +++ b/docs/documentation/atvscript.md @@ -227,6 +227,11 @@ $ atvscript -s 10.0.10.81 menu {"result": "success", "datetime": "2020-04-06T18:51:04.758569+02:00", "command": "menu"} ``` +`volume_up` and `volume_down` use the {% include api i="interface.Audio" %} +interface. `turn_on` and `turn_off` are also supported through +{% include api i="interface.Power" %}. These commands return the same success +output as remote control buttons. + ## Push, Power, Audio and Keyboard Updates Push, power, audio and keyboard updates are printed to the terminal as they happen: diff --git a/pyatv/scripts/atvscript.py b/pyatv/scripts/atvscript.py index 01ef6d2e1..552ac0660 100644 --- a/pyatv/scripts/atvscript.py +++ b/pyatv/scripts/atvscript.py @@ -329,6 +329,14 @@ async def _run_command(atv, args, abort_sem, loop): await wait_for_input(loop, abort_sem) return output(True, values={"push_updates": "finished"}) + if args.command in ("volume_up", "volume_down"): + await getattr(atv.audio, args.command)() + return output(True, values={"command": args.command}) + + if args.command in ("turn_on", "turn_off"): + await getattr(atv.power, args.command)() + return output(True, values={"command": args.command}) + if args.command in retrieve_commands(RemoteControl): await getattr(atv.remote_control, args.command)() return output(True, values={"command": args.command}) diff --git a/tests/scripts/test_atvscript.py b/tests/scripts/test_atvscript.py index f2be15efc..984e827d5 100644 --- a/tests/scripts/test_atvscript.py +++ b/tests/scripts/test_atvscript.py @@ -2,11 +2,14 @@ import json import logging +from types import SimpleNamespace +from unittest.mock import AsyncMock from deepdiff import DeepDiff import pytest from pyatv.const import Protocol +from pyatv.scripts import atvscript from tests.scripts.conftest import AIRPLAY_ID, DMAP_ID, IP_1, IP_2, MRP_ID @@ -107,3 +110,36 @@ async def test_mrp_idle(scriptenv): }, ) assert exit_code == 0 + + +@pytest.mark.parametrize( + "command,interface", + [ + ("volume_up", "audio"), + ("volume_down", "audio"), + ("turn_on", "power"), + ("turn_off", "power"), + ("play", "remote_control"), + ], +) +async def test_command_interface(command, interface): + atv = SimpleNamespace( + audio=AsyncMock(), power=AsyncMock(), remote_control=AsyncMock() + ) + result = await atvscript._run_command( + atv, SimpleNamespace(command=command), None, None + ) + assert_json_output(json.dumps(result), {"result": "success", "command": command}) + getattr(getattr(atv, interface), command).assert_awaited_once_with() + if interface != "remote_control": + getattr(atv.remote_control, command).assert_not_awaited() + + +@pytest.mark.parametrize("command", ["volume", "set_volume", "power_state", "unknown"]) +async def test_unsupported_command(command): + result = await atvscript._run_command( + SimpleNamespace(), SimpleNamespace(command=command), None, None + ) + assert_json_output( + json.dumps(result), {"result": "failure", "error": "unsupported_command"} + )