Skip to content

Commit c61f2e9

Browse files
authored
Add --child option to feature command (python-kasa#789)
This allows listing and changing child device features that were previously not accessible using the cli tool.
1 parent 7884436 commit c61f2e9

File tree

3 files changed

+126
-6
lines changed

3 files changed

+126
-6
lines changed

kasa/cli.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,22 +1156,39 @@ async def shell(dev: Device):
11561156
@cli.command(name="feature")
11571157
@click.argument("name", required=False)
11581158
@click.argument("value", required=False)
1159+
@click.option("--child", required=False)
11591160
@pass_dev
1160-
async def feature(dev, name: str, value):
1161+
async def feature(dev: Device, child: str, name: str, value):
11611162
"""Access and modify features.
11621163
11631164
If no *name* is given, lists available features and their values.
11641165
If only *name* is given, the value of named feature is returned.
11651166
If both *name* and *value* are set, the described setting is changed.
11661167
"""
1168+
if child is not None:
1169+
echo(f"Targeting child device {child}")
1170+
dev = dev.get_child_device(child)
11671171
if not name:
1172+
1173+
def _print_features(dev):
1174+
for name, feat in dev.features.items():
1175+
try:
1176+
echo(f"\t{feat.name} ({name}): {feat.value}")
1177+
except Exception as ex:
1178+
echo(f"\t{feat.name} ({name}): [red]{ex}[/red]")
1179+
11681180
echo("[bold]== Features ==[/bold]")
1169-
for name, feat in dev.features.items():
1170-
echo(f"{feat.name} ({name}): {feat.value}")
1181+
_print_features(dev)
1182+
1183+
if dev.children:
1184+
for child_dev in dev.children:
1185+
echo(f"[bold]== Child {child_dev.alias} ==")
1186+
_print_features(child_dev)
1187+
11711188
return
11721189

11731190
if name not in dev.features:
1174-
echo(f"No feature by name {name}")
1191+
echo(f"No feature by name '{name}'")
11751192
return
11761193

11771194
feat = dev.features[name]

kasa/tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,7 @@ async def mock_discover(self):
573573
yield discovery_data
574574

575575

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

kasa/tests/test_cli.py

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,14 @@
3232
from kasa.discover import Discover, DiscoveryResult
3333
from kasa.iot import IotDevice
3434

35-
from .conftest import device_iot, device_smart, handle_turn_on, new_discovery, turn_on
35+
from .conftest import (
36+
device_iot,
37+
device_smart,
38+
get_device_for_file,
39+
handle_turn_on,
40+
new_discovery,
41+
turn_on,
42+
)
3643

3744

3845
async def test_update_called_by_cli(dev, mocker):
@@ -684,3 +691,99 @@ async def test_errors(mocker):
684691
)
685692
assert res.exit_code == 2
686693
assert "Raised error:" not in res.output
694+
695+
696+
async def test_feature(mocker):
697+
"""Test feature command."""
698+
dummy_device = await get_device_for_file("P300(EU)_1.0_1.0.13.json", "SMART")
699+
mocker.patch("kasa.discover.Discover.discover_single", return_value=dummy_device)
700+
runner = CliRunner()
701+
res = await runner.invoke(
702+
cli,
703+
["--host", "127.0.0.123", "--debug", "feature"],
704+
catch_exceptions=False,
705+
)
706+
assert "LED" in res.output
707+
assert "== Child " in res.output # child listing
708+
709+
assert res.exit_code == 0
710+
711+
712+
async def test_feature_single(mocker):
713+
"""Test feature command returning single value."""
714+
dummy_device = await get_device_for_file("P300(EU)_1.0_1.0.13.json", "SMART")
715+
mocker.patch("kasa.discover.Discover.discover_single", return_value=dummy_device)
716+
runner = CliRunner()
717+
res = await runner.invoke(
718+
cli,
719+
["--host", "127.0.0.123", "--debug", "feature", "led"],
720+
catch_exceptions=False,
721+
)
722+
assert "LED" in res.output
723+
assert "== Features ==" not in res.output
724+
assert res.exit_code == 0
725+
726+
async def test_feature_missing(mocker):
727+
"""Test feature command returning single value."""
728+
dummy_device = await get_device_for_file("P300(EU)_1.0_1.0.13.json", "SMART")
729+
mocker.patch("kasa.discover.Discover.discover_single", return_value=dummy_device)
730+
runner = CliRunner()
731+
res = await runner.invoke(
732+
cli,
733+
["--host", "127.0.0.123", "--debug", "feature", "missing"],
734+
catch_exceptions=False,
735+
)
736+
assert "No feature by name 'missing'" in res.output
737+
assert "== Features ==" not in res.output
738+
assert res.exit_code == 0
739+
740+
async def test_feature_set(mocker):
741+
"""Test feature command's set value."""
742+
dummy_device = await get_device_for_file("P300(EU)_1.0_1.0.13.json", "SMART")
743+
led_setter = mocker.patch("kasa.smart.modules.ledmodule.LedModule.set_led")
744+
mocker.patch("kasa.discover.Discover.discover_single", return_value=dummy_device)
745+
746+
runner = CliRunner()
747+
res = await runner.invoke(
748+
cli,
749+
["--host", "127.0.0.123", "--debug", "feature", "led", "True"],
750+
catch_exceptions=False,
751+
)
752+
753+
led_setter.assert_called_with(True)
754+
assert "Setting led to True" in res.output
755+
assert res.exit_code == 0
756+
757+
758+
async def test_feature_set_child(mocker):
759+
"""Test feature command's set value."""
760+
dummy_device = await get_device_for_file("P300(EU)_1.0_1.0.13.json", "SMART")
761+
setter = mocker.patch("kasa.smart.smartdevice.SmartDevice.set_state")
762+
763+
mocker.patch("kasa.discover.Discover.discover_single", return_value=dummy_device)
764+
get_child_device = mocker.spy(dummy_device, "get_child_device")
765+
766+
child_id = "000000000000000000000000000000000000000001"
767+
768+
runner = CliRunner()
769+
res = await runner.invoke(
770+
cli,
771+
[
772+
"--host",
773+
"127.0.0.123",
774+
"--debug",
775+
"feature",
776+
"--child",
777+
child_id,
778+
"state",
779+
"False",
780+
],
781+
catch_exceptions=False,
782+
)
783+
784+
get_child_device.assert_called()
785+
setter.assert_called_with(False)
786+
787+
assert f"Targeting child device {child_id}"
788+
assert "Setting state to False" in res.output
789+
assert res.exit_code == 0

0 commit comments

Comments
 (0)