cli: print model, https, and lv for discover list (#1339)

```
kasa --target 192.168.xx.xx discover list

HOST            MODEL     DEVICE FAMILY        ENCRYPT HTTPS LV  ALIAS
192.168.xxx.xxx KP115(EU) IOT.SMARTPLUGSWITCH  XOR     0     -   Fridge
192.168.xxx.xxx L900-5    SMART.TAPOBULB       KLAP    0     2   L900
192.168.xxx.xxx P115      SMART.TAPOPLUG       AES     0     2   Nightdesk
192.168.xxx.xxx TC65      SMART.IPCAMERA       AES     1     2   Tapo_TC65_B593

```

Also handles `TimeoutError` and `Exception` during `update()`

---------

Co-authored-by: Steven B <51370195+sdb9696@users.noreply.github.com>
This commit is contained in:
Teemu R. 2024-12-17 21:09:17 +01:00 committed by GitHub
parent 14d5629de1
commit 37ef7b0463
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 64 additions and 13 deletions

View File

@ -123,14 +123,19 @@ async def list(ctx):
async def print_discovered(dev: Device): async def print_discovered(dev: Device):
cparams = dev.config.connection_type cparams = dev.config.connection_type
infostr = ( infostr = (
f"{dev.host:<15} {cparams.device_family.value:<20} " f"{dev.host:<15} {dev.model:<9} {cparams.device_family.value:<20} "
f"{cparams.encryption_type.value:<7}" f"{cparams.encryption_type.value:<7} {cparams.https:<5} "
f"{cparams.login_version or '-':<3}"
) )
async with sem: async with sem:
try: try:
await dev.update() await dev.update()
except AuthenticationError: except AuthenticationError:
echo(f"{infostr} - Authentication failed") echo(f"{infostr} - Authentication failed")
except TimeoutError:
echo(f"{infostr} - Timed out")
except Exception as ex:
echo(f"{infostr} - Error: {ex}")
else: else:
echo(f"{infostr} {dev.alias}") echo(f"{infostr} {dev.alias}")
@ -138,7 +143,10 @@ async def list(ctx):
if host := unsupported_exception.host: if host := unsupported_exception.host:
echo(f"{host:<15} UNSUPPORTED DEVICE") echo(f"{host:<15} UNSUPPORTED DEVICE")
echo(f"{'HOST':<15} {'DEVICE FAMILY':<20} {'ENCRYPT':<7} {'ALIAS'}") echo(
f"{'HOST':<15} {'MODEL':<9} {'DEVICE FAMILY':<20} {'ENCRYPT':<7} "
f"{'HTTPS':<5} {'LV':<3} {'ALIAS'}"
)
return await _discover( return await _discover(
ctx, ctx,
print_discovered=print_discovered, print_discovered=print_discovered,

View File

@ -160,6 +160,17 @@ def create_discovery_mock(ip: str, fixture_data: dict):
login_version: int | None = None login_version: int | None = None
port_override: int | None = None port_override: int | None = None
@property
def model(self) -> str:
dd = self.discovery_data
model_region = (
dd["result"]["device_model"]
if self.discovery_port == 20002
else dd["system"]["get_sysinfo"]["model"]
)
model, _, _ = model_region.partition("(")
return model
@property @property
def _datagram(self) -> bytes: def _datagram(self) -> bytes:
if self.default_port == 9999: if self.default_port == 9999:
@ -178,7 +189,10 @@ def create_discovery_mock(ip: str, fixture_data: dict):
"encrypt_type", discovery_result.get("encrypt_info", {}).get("sym_schm") "encrypt_type", discovery_result.get("encrypt_info", {}).get("sym_schm")
) )
login_version = discovery_result["mgt_encrypt_schm"].get("lv") if not (login_version := discovery_result["mgt_encrypt_schm"].get("lv")) and (
et := discovery_result.get("encrypt_type")
):
login_version = max([int(i) for i in et])
https = discovery_result["mgt_encrypt_schm"]["is_support_https"] https = discovery_result["mgt_encrypt_schm"]["is_support_https"]
dm = _DiscoveryMock( dm = _DiscoveryMock(
ip, ip,

View File

@ -122,8 +122,15 @@ async def test_list_devices(discovery_mock, runner):
catch_exceptions=False, catch_exceptions=False,
) )
assert res.exit_code == 0 assert res.exit_code == 0
header = f"{'HOST':<15} {'DEVICE FAMILY':<20} {'ENCRYPT':<7} {'ALIAS'}" header = (
row = f"{discovery_mock.ip:<15} {discovery_mock.device_type:<20} {discovery_mock.encrypt_type:<7}" f"{'HOST':<15} {'MODEL':<9} {'DEVICE FAMILY':<20} {'ENCRYPT':<7} "
f"{'HTTPS':<5} {'LV':<3} {'ALIAS'}"
)
row = (
f"{discovery_mock.ip:<15} {discovery_mock.model:<9} {discovery_mock.device_type:<20} "
f"{discovery_mock.encrypt_type:<7} {discovery_mock.https:<5} "
f"{discovery_mock.login_version or '-':<3}"
)
assert header in res.output assert header in res.output
assert row in res.output assert row in res.output
@ -158,14 +165,26 @@ async def test_discover_raw(discovery_mock, runner, mocker):
redact_spy.assert_called() redact_spy.assert_called()
@pytest.mark.parametrize(
("exception", "expected"),
[
pytest.param(
AuthenticationError("Failed to authenticate"),
"Authentication failed",
id="auth",
),
pytest.param(TimeoutError(), "Timed out", id="timeout"),
pytest.param(Exception("Foobar"), "Error: Foobar", id="other-error"),
],
)
@new_discovery @new_discovery
async def test_list_auth_failed(discovery_mock, mocker, runner): async def test_list_update_failed(discovery_mock, mocker, runner, exception, expected):
"""Test that device update is called on main.""" """Test that device update is called on main."""
device_class = Discover._get_device_class(discovery_mock.discovery_data) device_class = Discover._get_device_class(discovery_mock.discovery_data)
mocker.patch.object( mocker.patch.object(
device_class, device_class,
"update", "update",
side_effect=AuthenticationError("Failed to authenticate"), side_effect=exception,
) )
res = await runner.invoke( res = await runner.invoke(
cli, cli,
@ -173,10 +192,17 @@ async def test_list_auth_failed(discovery_mock, mocker, runner):
catch_exceptions=False, catch_exceptions=False,
) )
assert res.exit_code == 0 assert res.exit_code == 0
header = f"{'HOST':<15} {'DEVICE FAMILY':<20} {'ENCRYPT':<7} {'ALIAS'}" header = (
row = f"{discovery_mock.ip:<15} {discovery_mock.device_type:<20} {discovery_mock.encrypt_type:<7} - Authentication failed" f"{'HOST':<15} {'MODEL':<9} {'DEVICE FAMILY':<20} {'ENCRYPT':<7} "
assert header in res.output f"{'HTTPS':<5} {'LV':<3} {'ALIAS'}"
assert row in res.output )
row = (
f"{discovery_mock.ip:<15} {discovery_mock.model:<9} {discovery_mock.device_type:<20} "
f"{discovery_mock.encrypt_type:<7} {discovery_mock.https:<5} "
f"{discovery_mock.login_version or '-':<3} - {expected}"
)
assert header in res.output.replace("\n", "")
assert row in res.output.replace("\n", "")
async def test_list_unsupported(unsupported_device_info, runner): async def test_list_unsupported(unsupported_device_info, runner):
@ -187,7 +213,10 @@ async def test_list_unsupported(unsupported_device_info, runner):
catch_exceptions=False, catch_exceptions=False,
) )
assert res.exit_code == 0 assert res.exit_code == 0
header = f"{'HOST':<15} {'DEVICE FAMILY':<20} {'ENCRYPT':<7} {'ALIAS'}" header = (
f"{'HOST':<15} {'MODEL':<9} {'DEVICE FAMILY':<20} {'ENCRYPT':<7} "
f"{'HTTPS':<5} {'LV':<3} {'ALIAS'}"
)
row = f"{'127.0.0.1':<15} UNSUPPORTED DEVICE" row = f"{'127.0.0.1':<15} UNSUPPORTED DEVICE"
assert header in res.output assert header in res.output
assert row in res.output assert row in res.output