Add --child option to feature command (#789)

This allows listing and changing child device features that were previously not accessible using the cli tool.
This commit is contained in:
Teemu R 2024-02-23 23:32:17 +01:00 committed by GitHub
parent 7884436679
commit c61f2e931c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 126 additions and 6 deletions

View File

@ -1156,22 +1156,39 @@ async def shell(dev: Device):
@cli.command(name="feature") @cli.command(name="feature")
@click.argument("name", required=False) @click.argument("name", required=False)
@click.argument("value", required=False) @click.argument("value", required=False)
@click.option("--child", required=False)
@pass_dev @pass_dev
async def feature(dev, name: str, value): async def feature(dev: Device, child: str, name: str, value):
"""Access and modify features. """Access and modify features.
If no *name* is given, lists available features and their values. If no *name* is given, lists available features and their values.
If only *name* is given, the value of named feature is returned. If only *name* is given, the value of named feature is returned.
If both *name* and *value* are set, the described setting is changed. If both *name* and *value* are set, the described setting is changed.
""" """
if child is not None:
echo(f"Targeting child device {child}")
dev = dev.get_child_device(child)
if not name: if not name:
echo("[bold]== Features ==[/bold]")
def _print_features(dev):
for name, feat in dev.features.items(): for name, feat in dev.features.items():
echo(f"{feat.name} ({name}): {feat.value}") try:
echo(f"\t{feat.name} ({name}): {feat.value}")
except Exception as ex:
echo(f"\t{feat.name} ({name}): [red]{ex}[/red]")
echo("[bold]== Features ==[/bold]")
_print_features(dev)
if dev.children:
for child_dev in dev.children:
echo(f"[bold]== Child {child_dev.alias} ==")
_print_features(child_dev)
return return
if name not in dev.features: if name not in dev.features:
echo(f"No feature by name {name}") echo(f"No feature by name '{name}'")
return return
feat = dev.features[name] feat = dev.features[name]

View File

@ -573,7 +573,7 @@ def unsupported_device_info(request, mocker):
yield discovery_data yield discovery_data
@pytest.fixture() @pytest.fixture
def dummy_protocol(): def dummy_protocol():
"""Return a smart protocol instance with a mocking-ready dummy transport.""" """Return a smart protocol instance with a mocking-ready dummy transport."""

View File

@ -32,7 +32,14 @@ from kasa.cli import (
from kasa.discover import Discover, DiscoveryResult from kasa.discover import Discover, DiscoveryResult
from kasa.iot import IotDevice from kasa.iot import IotDevice
from .conftest import device_iot, device_smart, handle_turn_on, new_discovery, turn_on from .conftest import (
device_iot,
device_smart,
get_device_for_file,
handle_turn_on,
new_discovery,
turn_on,
)
async def test_update_called_by_cli(dev, mocker): async def test_update_called_by_cli(dev, mocker):
@ -684,3 +691,99 @@ async def test_errors(mocker):
) )
assert res.exit_code == 2 assert res.exit_code == 2
assert "Raised error:" not in res.output assert "Raised error:" not in res.output
async def test_feature(mocker):
"""Test feature command."""
dummy_device = await get_device_for_file("P300(EU)_1.0_1.0.13.json", "SMART")
mocker.patch("kasa.discover.Discover.discover_single", return_value=dummy_device)
runner = CliRunner()
res = await runner.invoke(
cli,
["--host", "127.0.0.123", "--debug", "feature"],
catch_exceptions=False,
)
assert "LED" in res.output
assert "== Child " in res.output # child listing
assert res.exit_code == 0
async def test_feature_single(mocker):
"""Test feature command returning single value."""
dummy_device = await get_device_for_file("P300(EU)_1.0_1.0.13.json", "SMART")
mocker.patch("kasa.discover.Discover.discover_single", return_value=dummy_device)
runner = CliRunner()
res = await runner.invoke(
cli,
["--host", "127.0.0.123", "--debug", "feature", "led"],
catch_exceptions=False,
)
assert "LED" in res.output
assert "== Features ==" not in res.output
assert res.exit_code == 0
async def test_feature_missing(mocker):
"""Test feature command returning single value."""
dummy_device = await get_device_for_file("P300(EU)_1.0_1.0.13.json", "SMART")
mocker.patch("kasa.discover.Discover.discover_single", return_value=dummy_device)
runner = CliRunner()
res = await runner.invoke(
cli,
["--host", "127.0.0.123", "--debug", "feature", "missing"],
catch_exceptions=False,
)
assert "No feature by name 'missing'" in res.output
assert "== Features ==" not in res.output
assert res.exit_code == 0
async def test_feature_set(mocker):
"""Test feature command's set value."""
dummy_device = await get_device_for_file("P300(EU)_1.0_1.0.13.json", "SMART")
led_setter = mocker.patch("kasa.smart.modules.ledmodule.LedModule.set_led")
mocker.patch("kasa.discover.Discover.discover_single", return_value=dummy_device)
runner = CliRunner()
res = await runner.invoke(
cli,
["--host", "127.0.0.123", "--debug", "feature", "led", "True"],
catch_exceptions=False,
)
led_setter.assert_called_with(True)
assert "Setting led to True" in res.output
assert res.exit_code == 0
async def test_feature_set_child(mocker):
"""Test feature command's set value."""
dummy_device = await get_device_for_file("P300(EU)_1.0_1.0.13.json", "SMART")
setter = mocker.patch("kasa.smart.smartdevice.SmartDevice.set_state")
mocker.patch("kasa.discover.Discover.discover_single", return_value=dummy_device)
get_child_device = mocker.spy(dummy_device, "get_child_device")
child_id = "000000000000000000000000000000000000000001"
runner = CliRunner()
res = await runner.invoke(
cli,
[
"--host",
"127.0.0.123",
"--debug",
"feature",
"--child",
child_id,
"state",
"False",
],
catch_exceptions=False,
)
get_child_device.assert_called()
setter.assert_called_with(False)
assert f"Targeting child device {child_id}"
assert "Setting state to False" in res.output
assert res.exit_code == 0