Skip to content
Merged
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
25 changes: 21 additions & 4 deletions kasa/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1156,22 +1156,39 @@ async def shell(dev: Device):
@cli.command(name="feature")
@click.argument("name", required=False)
@click.argument("value", required=False)
@click.option("--child", required=False)
@pass_dev
async def feature(dev, name: str, value):
async def feature(dev: Device, child: str, name: str, value):
"""Access and modify features.

If no *name* is given, lists available features and their values.
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 child is not None:
echo(f"Targeting child device {child}")
dev = dev.get_child_device(child)
if not name:

def _print_features(dev):
for name, feat in dev.features.items():
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]")
for name, feat in dev.features.items():
echo(f"{feat.name} ({name}): {feat.value}")
_print_features(dev)

if dev.children:
for child_dev in dev.children:
echo(f"[bold]== Child {child_dev.alias} ==")
_print_features(child_dev)

return

if name not in dev.features:
echo(f"No feature by name {name}")
echo(f"No feature by name '{name}'")
return

feat = dev.features[name]
Expand Down
2 changes: 1 addition & 1 deletion kasa/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ async def mock_discover(self):
yield discovery_data


@pytest.fixture()
@pytest.fixture
def dummy_protocol():
"""Return a smart protocol instance with a mocking-ready dummy transport."""

Expand Down
105 changes: 104 additions & 1 deletion kasa/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@
from kasa.discover import Discover, DiscoveryResult
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):
Expand Down Expand Up @@ -684,3 +691,99 @@ async def test_errors(mocker):
)
assert res.exit_code == 2
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