mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-22 19:23:34 +00:00
Improve CLI Discovery output (#583)
- Show discovery results for unsupported devices and devices that fail to authenticate. - Rename `--show-unsupported` to `--verbose`. - Remove separate `--timeout` parameter from cli discovery so it's not confused with `--timeout` now added to cli command. - Add tests.
This commit is contained in:
parent
2e6c41d039
commit
209391c422
102
kasa/cli.py
102
kasa/cli.py
@ -18,8 +18,15 @@ from kasa import (
|
|||||||
SmartBulb,
|
SmartBulb,
|
||||||
SmartDevice,
|
SmartDevice,
|
||||||
SmartStrip,
|
SmartStrip,
|
||||||
|
UnsupportedDeviceException,
|
||||||
)
|
)
|
||||||
from kasa.device_factory import DEVICE_TYPE_TO_CLASS
|
from kasa.device_factory import DEVICE_TYPE_TO_CLASS
|
||||||
|
from kasa.discover import DiscoveryResult
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pydantic.v1 import ValidationError
|
||||||
|
except ImportError:
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from rich import print as _do_echo
|
from rich import print as _do_echo
|
||||||
@ -241,7 +248,7 @@ async def cli(
|
|||||||
|
|
||||||
if host is None:
|
if host is None:
|
||||||
echo("No host name given, trying discovery..")
|
echo("No host name given, trying discovery..")
|
||||||
return await ctx.invoke(discover, timeout=discovery_timeout)
|
return await ctx.invoke(discover)
|
||||||
|
|
||||||
if type is not None:
|
if type is not None:
|
||||||
device_type = DeviceType.from_value(type)
|
device_type = DeviceType.from_value(type)
|
||||||
@ -300,21 +307,21 @@ async def join(dev: SmartDevice, ssid, password, keytype):
|
|||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option("--timeout", default=3, required=False)
|
|
||||||
@click.option(
|
@click.option(
|
||||||
"--show-unsupported",
|
"--verbose",
|
||||||
envvar="KASA_SHOW_UNSUPPORTED",
|
envvar="KASA_VERBOSE",
|
||||||
required=False,
|
required=False,
|
||||||
default=False,
|
default=False,
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
help="Print out discovered unsupported devices",
|
help="Be more verbose on output",
|
||||||
)
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
async def discover(ctx, timeout, show_unsupported):
|
async def discover(ctx, verbose):
|
||||||
"""Discover devices in the network."""
|
"""Discover devices in the network."""
|
||||||
target = ctx.parent.params["target"]
|
target = ctx.parent.params["target"]
|
||||||
username = ctx.parent.params["username"]
|
username = ctx.parent.params["username"]
|
||||||
password = ctx.parent.params["password"]
|
password = ctx.parent.params["password"]
|
||||||
|
timeout = ctx.parent.params["discovery_timeout"]
|
||||||
|
|
||||||
credentials = Credentials(username, password)
|
credentials = Credentials(username, password)
|
||||||
|
|
||||||
@ -323,24 +330,37 @@ async def discover(ctx, timeout, show_unsupported):
|
|||||||
unsupported = []
|
unsupported = []
|
||||||
auth_failed = []
|
auth_failed = []
|
||||||
|
|
||||||
async def print_unsupported(data: str):
|
async def print_unsupported(unsupported_exception: UnsupportedDeviceException):
|
||||||
unsupported.append(data)
|
unsupported.append(unsupported_exception)
|
||||||
if show_unsupported:
|
async with sem:
|
||||||
echo(f"Found unsupported device (tapo/unknown encryption): {data}")
|
if unsupported_exception.discovery_result:
|
||||||
echo()
|
echo("== Unsupported device ==")
|
||||||
|
_echo_discovery_info(unsupported_exception.discovery_result)
|
||||||
|
echo()
|
||||||
|
else:
|
||||||
|
echo("== Unsupported device ==")
|
||||||
|
echo(f"\t{unsupported_exception}")
|
||||||
|
echo()
|
||||||
|
|
||||||
echo(f"Discovering devices on {target} for {timeout} seconds")
|
echo(f"Discovering devices on {target} for {timeout} seconds")
|
||||||
|
|
||||||
async def print_discovered(dev: SmartDevice):
|
async def print_discovered(dev: SmartDevice):
|
||||||
try:
|
async with sem:
|
||||||
await dev.update()
|
try:
|
||||||
async with sem:
|
await dev.update()
|
||||||
|
except AuthenticationException:
|
||||||
|
auth_failed.append(dev._discovery_info)
|
||||||
|
echo("== Authentication failed for device ==")
|
||||||
|
_echo_discovery_info(dev._discovery_info)
|
||||||
|
echo()
|
||||||
|
else:
|
||||||
discovered[dev.host] = dev.internal_state
|
discovered[dev.host] = dev.internal_state
|
||||||
ctx.obj = dev
|
ctx.obj = dev
|
||||||
await ctx.invoke(state)
|
await ctx.invoke(state)
|
||||||
echo()
|
if verbose:
|
||||||
except AuthenticationException as aex:
|
echo()
|
||||||
auth_failed.append(str(aex))
|
_echo_discovery_info(dev._discovery_info)
|
||||||
|
echo()
|
||||||
|
|
||||||
await Discover.discover(
|
await Discover.discover(
|
||||||
target=target,
|
target=target,
|
||||||
@ -352,22 +372,50 @@ async def discover(ctx, timeout, show_unsupported):
|
|||||||
|
|
||||||
echo(f"Found {len(discovered)} devices")
|
echo(f"Found {len(discovered)} devices")
|
||||||
if unsupported:
|
if unsupported:
|
||||||
echo(
|
echo(f"Found {len(unsupported)} unsupported devices")
|
||||||
f"Found {len(unsupported)} unsupported devices"
|
|
||||||
+ (
|
|
||||||
""
|
|
||||||
if show_unsupported
|
|
||||||
else ", to show them use: kasa discover --show-unsupported"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if auth_failed:
|
if auth_failed:
|
||||||
echo(f"Found {len(auth_failed)} devices that failed to authenticate")
|
echo(f"Found {len(auth_failed)} devices that failed to authenticate")
|
||||||
for fail in auth_failed:
|
|
||||||
echo(fail)
|
|
||||||
|
|
||||||
return discovered
|
return discovered
|
||||||
|
|
||||||
|
|
||||||
|
def _echo_dictionary(discovery_info: dict):
|
||||||
|
echo("\t[bold]== Discovery information ==[/bold]")
|
||||||
|
for key, value in discovery_info.items():
|
||||||
|
key_name = " ".join(x.capitalize() or "_" for x in key.split("_"))
|
||||||
|
key_name_and_spaces = "{:<15}".format(key_name + ":")
|
||||||
|
echo(f"\t{key_name_and_spaces}{value}")
|
||||||
|
|
||||||
|
|
||||||
|
def _echo_discovery_info(discovery_info):
|
||||||
|
if "system" in discovery_info and "get_sysinfo" in discovery_info["system"]:
|
||||||
|
_echo_dictionary(discovery_info["system"]["get_sysinfo"])
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
dr = DiscoveryResult(**discovery_info)
|
||||||
|
except ValidationError:
|
||||||
|
_echo_dictionary(discovery_info)
|
||||||
|
return
|
||||||
|
|
||||||
|
echo("\t[bold]== Discovery Result ==[/bold]")
|
||||||
|
echo(f"\tDevice Type: {dr.device_type}")
|
||||||
|
echo(f"\tDevice Model: {dr.device_model}")
|
||||||
|
echo(f"\tIP: {dr.ip}")
|
||||||
|
echo(f"\tMAC: {dr.mac}")
|
||||||
|
echo(f"\tDevice Id (hash): {dr.device_id}")
|
||||||
|
echo(f"\tOwner (hash): {dr.owner}")
|
||||||
|
echo(f"\tHW Ver: {dr.hw_ver}")
|
||||||
|
echo(f"\tIs Support IOT Cloud: {dr.is_support_iot_cloud})")
|
||||||
|
echo(f"\tOBD Src: {dr.obd_src}")
|
||||||
|
echo(f"\tFactory Default: {dr.factory_default}")
|
||||||
|
echo("\t\t== Encryption Scheme ==")
|
||||||
|
echo(f"\t\tEncrypt Type: {dr.mgt_encrypt_schm.encrypt_type}")
|
||||||
|
echo(f"\t\tIs Support HTTPS: {dr.mgt_encrypt_schm.is_support_https}")
|
||||||
|
echo(f"\t\tHTTP Port: {dr.mgt_encrypt_schm.http_port}")
|
||||||
|
echo(f"\t\tLV (Login Level): {dr.mgt_encrypt_schm.lv}")
|
||||||
|
|
||||||
|
|
||||||
async def find_host_from_alias(alias, target="255.255.255.255", timeout=1, attempts=3):
|
async def find_host_from_alias(alias, target="255.255.255.255", timeout=1, attempts=3):
|
||||||
"""Discover a device identified by its alias."""
|
"""Discover a device identified by its alias."""
|
||||||
for _attempt in range(1, attempts):
|
for _attempt in range(1, attempts):
|
||||||
|
@ -50,7 +50,9 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
|
|||||||
target: str = "255.255.255.255",
|
target: str = "255.255.255.255",
|
||||||
discovery_packets: int = 3,
|
discovery_packets: int = 3,
|
||||||
interface: Optional[str] = None,
|
interface: Optional[str] = None,
|
||||||
on_unsupported: Optional[Callable[[str], Awaitable[None]]] = None,
|
on_unsupported: Optional[
|
||||||
|
Callable[[UnsupportedDeviceException], Awaitable[None]]
|
||||||
|
] = None,
|
||||||
port: Optional[int] = None,
|
port: Optional[int] = None,
|
||||||
discovered_event: Optional[asyncio.Event] = None,
|
discovered_event: Optional[asyncio.Event] = None,
|
||||||
credentials: Optional[Credentials] = None,
|
credentials: Optional[Credentials] = None,
|
||||||
@ -64,7 +66,7 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
|
|||||||
self.target = (target, self.discovery_port)
|
self.target = (target, self.discovery_port)
|
||||||
self.target_2 = (target, Discover.DISCOVERY_PORT_2)
|
self.target_2 = (target, Discover.DISCOVERY_PORT_2)
|
||||||
self.discovered_devices = {}
|
self.discovered_devices = {}
|
||||||
self.unsupported_devices: Dict = {}
|
self.unsupported_device_exceptions: Dict = {}
|
||||||
self.invalid_device_exceptions: Dict = {}
|
self.invalid_device_exceptions: Dict = {}
|
||||||
self.on_unsupported = on_unsupported
|
self.on_unsupported = on_unsupported
|
||||||
self.discovered_event = discovered_event
|
self.discovered_event = discovered_event
|
||||||
@ -119,9 +121,9 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
|
|||||||
return
|
return
|
||||||
except UnsupportedDeviceException as udex:
|
except UnsupportedDeviceException as udex:
|
||||||
_LOGGER.debug("Unsupported device found at %s << %s", ip, udex)
|
_LOGGER.debug("Unsupported device found at %s << %s", ip, udex)
|
||||||
self.unsupported_devices[ip] = str(udex)
|
self.unsupported_device_exceptions[ip] = udex
|
||||||
if self.on_unsupported is not None:
|
if self.on_unsupported is not None:
|
||||||
asyncio.ensure_future(self.on_unsupported(str(udex)))
|
asyncio.ensure_future(self.on_unsupported(udex))
|
||||||
if self.discovered_event is not None:
|
if self.discovered_event is not None:
|
||||||
self.discovered_event.set()
|
self.discovered_event.set()
|
||||||
return
|
return
|
||||||
@ -336,10 +338,8 @@ class Discover:
|
|||||||
if update_parent_devices and dev.has_children:
|
if update_parent_devices and dev.has_children:
|
||||||
await dev.update()
|
await dev.update()
|
||||||
return dev
|
return dev
|
||||||
elif ip in protocol.unsupported_devices:
|
elif ip in protocol.unsupported_device_exceptions:
|
||||||
raise UnsupportedDeviceException(
|
raise protocol.unsupported_device_exceptions[ip]
|
||||||
f"Unsupported device {host}: {protocol.unsupported_devices[ip]}"
|
|
||||||
)
|
|
||||||
elif ip in protocol.invalid_device_exceptions:
|
elif ip in protocol.invalid_device_exceptions:
|
||||||
raise protocol.invalid_device_exceptions[ip]
|
raise protocol.invalid_device_exceptions[ip]
|
||||||
else:
|
else:
|
||||||
@ -397,7 +397,8 @@ class Discover:
|
|||||||
if (device_class := get_device_class_from_type_name(type_)) is None:
|
if (device_class := get_device_class_from_type_name(type_)) is None:
|
||||||
_LOGGER.warning("Got unsupported device type: %s", type_)
|
_LOGGER.warning("Got unsupported device type: %s", type_)
|
||||||
raise UnsupportedDeviceException(
|
raise UnsupportedDeviceException(
|
||||||
f"Unsupported device {ip} of type {type_}: {info}"
|
f"Unsupported device {ip} of type {type_}: {info}",
|
||||||
|
discovery_result=discovery_result.get_dict(),
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
protocol := get_protocol_from_connection_name(
|
protocol := get_protocol_from_connection_name(
|
||||||
@ -406,7 +407,8 @@ class Discover:
|
|||||||
) is None:
|
) is None:
|
||||||
_LOGGER.warning("Got unsupported device type: %s", encrypt_type_)
|
_LOGGER.warning("Got unsupported device type: %s", encrypt_type_)
|
||||||
raise UnsupportedDeviceException(
|
raise UnsupportedDeviceException(
|
||||||
f"Unsupported encryption scheme {ip} of type {encrypt_type_}: {info}"
|
f"Unsupported encryption scheme {ip} of type {encrypt_type_}: {info}",
|
||||||
|
discovery_result=discovery_result.get_dict(),
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER.debug("[DISCOVERY] %s << %s", ip, info)
|
_LOGGER.debug("[DISCOVERY] %s << %s", ip, info)
|
||||||
|
@ -9,6 +9,10 @@ class SmartDeviceException(Exception):
|
|||||||
class UnsupportedDeviceException(SmartDeviceException):
|
class UnsupportedDeviceException(SmartDeviceException):
|
||||||
"""Exception for trying to connect to unsupported devices."""
|
"""Exception for trying to connect to unsupported devices."""
|
||||||
|
|
||||||
|
def __init__(self, *args, discovery_result=None):
|
||||||
|
self.discovery_result = discovery_result
|
||||||
|
super().__init__(args)
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationException(SmartDeviceException):
|
class AuthenticationException(SmartDeviceException):
|
||||||
"""Base exception for device authentication errors."""
|
"""Base exception for device authentication errors."""
|
||||||
|
@ -129,6 +129,37 @@ ALL_DEVICES = ALL_DEVICES_IOT.union(ALL_DEVICES_SMART)
|
|||||||
IP_MODEL_CACHE: Dict[str, str] = {}
|
IP_MODEL_CACHE: Dict[str, str] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _make_unsupported(device_family, encrypt_type):
|
||||||
|
return {
|
||||||
|
"result": {
|
||||||
|
"device_id": "xx",
|
||||||
|
"owner": "xx",
|
||||||
|
"device_type": device_family,
|
||||||
|
"device_model": "P110(EU)",
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"mac": "48-22xxx",
|
||||||
|
"is_support_iot_cloud": True,
|
||||||
|
"obd_src": "tplink",
|
||||||
|
"factory_default": False,
|
||||||
|
"mgt_encrypt_schm": {
|
||||||
|
"is_support_https": False,
|
||||||
|
"encrypt_type": encrypt_type,
|
||||||
|
"http_port": 80,
|
||||||
|
"lv": 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"error_code": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
UNSUPPORTED_DEVICES = {
|
||||||
|
"unknown_device_family": _make_unsupported("SMART.TAPOXMASTREE", "AES"),
|
||||||
|
"wrong_encryption_iot": _make_unsupported("IOT.SMARTPLUGSWITCH", "AES"),
|
||||||
|
"wrong_encryption_smart": _make_unsupported("SMART.TAPOBULB", "IOT"),
|
||||||
|
"unknown_encryption": _make_unsupported("IOT.SMARTPLUGSWITCH", "FOO"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def idgenerator(paramtuple):
|
def idgenerator(paramtuple):
|
||||||
try:
|
try:
|
||||||
return basename(paramtuple[0]) + (
|
return basename(paramtuple[0]) + (
|
||||||
@ -242,7 +273,7 @@ def filter_fixtures(desc, root_filter):
|
|||||||
def parametrize_discovery(desc, root_key):
|
def parametrize_discovery(desc, root_key):
|
||||||
filtered_fixtures = filter_fixtures(desc, root_key)
|
filtered_fixtures = filter_fixtures(desc, root_key)
|
||||||
return pytest.mark.parametrize(
|
return pytest.mark.parametrize(
|
||||||
"discovery_data",
|
"all_fixture_data",
|
||||||
filtered_fixtures.values(),
|
filtered_fixtures.values(),
|
||||||
indirect=True,
|
indirect=True,
|
||||||
ids=filtered_fixtures.keys(),
|
ids=filtered_fixtures.keys(),
|
||||||
@ -360,7 +391,7 @@ async def get_device_for_file(file, protocol):
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=SUPPORTED_DEVICES)
|
@pytest.fixture(params=SUPPORTED_DEVICES, ids=idgenerator)
|
||||||
async def dev(request):
|
async def dev(request):
|
||||||
"""Device fixture.
|
"""Device fixture.
|
||||||
|
|
||||||
@ -386,23 +417,27 @@ async def dev(request):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def discovery_mock(discovery_data, mocker):
|
def discovery_mock(all_fixture_data, mocker):
|
||||||
@dataclass
|
@dataclass
|
||||||
class _DiscoveryMock:
|
class _DiscoveryMock:
|
||||||
ip: str
|
ip: str
|
||||||
default_port: int
|
default_port: int
|
||||||
discovery_data: dict
|
discovery_data: dict
|
||||||
|
query_data: dict
|
||||||
port_override: Optional[int] = None
|
port_override: Optional[int] = None
|
||||||
|
|
||||||
if "result" in discovery_data:
|
if "discovery_result" in all_fixture_data:
|
||||||
|
discovery_data = {"result": all_fixture_data["discovery_result"]}
|
||||||
datagram = (
|
datagram = (
|
||||||
b"\x02\x00\x00\x01\x01[\x00\x00\x00\x00\x00\x00W\xcev\xf8"
|
b"\x02\x00\x00\x01\x01[\x00\x00\x00\x00\x00\x00W\xcev\xf8"
|
||||||
+ json_dumps(discovery_data).encode()
|
+ json_dumps(discovery_data).encode()
|
||||||
)
|
)
|
||||||
dm = _DiscoveryMock("127.0.0.123", 20002, discovery_data)
|
dm = _DiscoveryMock("127.0.0.123", 20002, discovery_data, all_fixture_data)
|
||||||
else:
|
else:
|
||||||
|
sys_info = all_fixture_data["system"]["get_sysinfo"]
|
||||||
|
discovery_data = {"system": {"get_sysinfo": sys_info}}
|
||||||
datagram = TPLinkSmartHomeProtocol.encrypt(json_dumps(discovery_data))[4:]
|
datagram = TPLinkSmartHomeProtocol.encrypt(json_dumps(discovery_data))[4:]
|
||||||
dm = _DiscoveryMock("127.0.0.123", 9999, discovery_data)
|
dm = _DiscoveryMock("127.0.0.123", 9999, discovery_data, all_fixture_data)
|
||||||
|
|
||||||
def mock_discover(self):
|
def mock_discover(self):
|
||||||
port = (
|
port = (
|
||||||
@ -420,17 +455,29 @@ def discovery_mock(discovery_data, mocker):
|
|||||||
"socket.getaddrinfo",
|
"socket.getaddrinfo",
|
||||||
side_effect=lambda *_, **__: [(None, None, None, None, (dm.ip, 0))],
|
side_effect=lambda *_, **__: [(None, None, None, None, (dm.ip, 0))],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if "component_nego" in dm.query_data:
|
||||||
|
proto = FakeSmartProtocol(dm.query_data)
|
||||||
|
else:
|
||||||
|
proto = FakeTransportProtocol(dm.query_data)
|
||||||
|
|
||||||
|
async def _query(request, retry_count: int = 3):
|
||||||
|
return await proto.query(request)
|
||||||
|
|
||||||
|
mocker.patch("kasa.IotProtocol.query", side_effect=_query)
|
||||||
|
mocker.patch("kasa.SmartProtocol.query", side_effect=_query)
|
||||||
|
mocker.patch("kasa.TPLinkSmartHomeProtocol.query", side_effect=_query)
|
||||||
|
|
||||||
yield dm
|
yield dm
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=FIXTURE_DATA.values(), ids=FIXTURE_DATA.keys(), scope="session")
|
@pytest.fixture
|
||||||
def discovery_data(request):
|
def discovery_data(all_fixture_data):
|
||||||
"""Return raw discovery file contents as JSON. Used for discovery tests."""
|
"""Return raw discovery file contents as JSON. Used for discovery tests."""
|
||||||
fixture_data = request.param
|
if "discovery_result" in all_fixture_data:
|
||||||
if "discovery_result" in fixture_data:
|
return {"result": all_fixture_data["discovery_result"]}
|
||||||
return {"result": fixture_data["discovery_result"]}
|
|
||||||
else:
|
else:
|
||||||
return {"system": {"get_sysinfo": fixture_data["system"]["get_sysinfo"]}}
|
return {"system": {"get_sysinfo": all_fixture_data["system"]["get_sysinfo"]}}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=FIXTURE_DATA.values(), ids=FIXTURE_DATA.keys(), scope="session")
|
@pytest.fixture(params=FIXTURE_DATA.values(), ids=FIXTURE_DATA.keys(), scope="session")
|
||||||
@ -440,6 +487,25 @@ def all_fixture_data(request):
|
|||||||
return fixture_data
|
return fixture_data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(params=UNSUPPORTED_DEVICES.values(), ids=UNSUPPORTED_DEVICES.keys())
|
||||||
|
def unsupported_device_info(request, mocker):
|
||||||
|
"""Return unsupported devices for cli and discovery tests."""
|
||||||
|
discovery_data = request.param
|
||||||
|
host = "127.0.0.1"
|
||||||
|
|
||||||
|
def mock_discover(self):
|
||||||
|
if discovery_data:
|
||||||
|
data = (
|
||||||
|
b"\x02\x00\x00\x01\x01[\x00\x00\x00\x00\x00\x00W\xcev\xf8"
|
||||||
|
+ json_dumps(discovery_data).encode()
|
||||||
|
)
|
||||||
|
self.datagram_received(data, (host, 20002))
|
||||||
|
|
||||||
|
mocker.patch("kasa.discover._DiscoverProtocol.do_discover", mock_discover)
|
||||||
|
|
||||||
|
yield discovery_data
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
parser.addoption(
|
parser.addoption(
|
||||||
"--ip", action="store", default=None, help="run against device on given ip"
|
"--ip", action="store", default=None, help="run against device on given ip"
|
||||||
|
@ -291,6 +291,13 @@ class FakeSmartProtocol(SmartProtocol):
|
|||||||
def __init__(self, info):
|
def __init__(self, info):
|
||||||
super().__init__("127.0.0.123", transport=FakeSmartTransport(info))
|
super().__init__("127.0.0.123", transport=FakeSmartTransport(info))
|
||||||
|
|
||||||
|
async def query(self, request, retry_count: int = 3):
|
||||||
|
"""Implement query here so can still patch SmartProtocol.query."""
|
||||||
|
resp_dict = await self._query(request, retry_count)
|
||||||
|
if "result" in resp_dict:
|
||||||
|
return resp_dict["result"]
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
class FakeSmartTransport(BaseTransport):
|
class FakeSmartTransport(BaseTransport):
|
||||||
def __init__(self, info):
|
def __init__(self, info):
|
||||||
|
@ -4,14 +4,12 @@ import asyncclick as click
|
|||||||
import pytest
|
import pytest
|
||||||
from asyncclick.testing import CliRunner
|
from asyncclick.testing import CliRunner
|
||||||
|
|
||||||
from kasa import SmartDevice, TPLinkSmartHomeProtocol
|
from kasa import AuthenticationException, SmartDevice, UnsupportedDeviceException
|
||||||
from kasa.cli import alias, brightness, cli, emeter, raw_command, state, sysinfo, toggle
|
from kasa.cli import alias, brightness, cli, emeter, raw_command, state, sysinfo, toggle
|
||||||
from kasa.device_factory import DEVICE_TYPE_TO_CLASS
|
from kasa.device_factory import DEVICE_TYPE_TO_CLASS
|
||||||
from kasa.discover import Discover
|
from kasa.discover import Discover
|
||||||
from kasa.smartprotocol import SmartProtocol
|
|
||||||
|
|
||||||
from .conftest import device_iot, handle_turn_on, new_discovery, turn_on
|
from .conftest import device_iot, handle_turn_on, new_discovery, turn_on
|
||||||
from .newfakes import FakeSmartProtocol, FakeTransportProtocol
|
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
@device_iot
|
||||||
@ -22,7 +20,6 @@ async def test_sysinfo(dev):
|
|||||||
assert dev.alias in res.output
|
assert dev.alias in res.output
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
@turn_on
|
@turn_on
|
||||||
async def test_state(dev, turn_on):
|
async def test_state(dev, turn_on):
|
||||||
await handle_turn_on(dev, turn_on)
|
await handle_turn_on(dev, turn_on)
|
||||||
@ -36,7 +33,6 @@ async def test_state(dev, turn_on):
|
|||||||
assert "Device state: False" in res.output
|
assert "Device state: False" in res.output
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
@turn_on
|
@turn_on
|
||||||
async def test_toggle(dev, turn_on, mocker):
|
async def test_toggle(dev, turn_on, mocker):
|
||||||
await handle_turn_on(dev, turn_on)
|
await handle_turn_on(dev, turn_on)
|
||||||
@ -226,3 +222,123 @@ async def test_duplicate_target_device():
|
|||||||
)
|
)
|
||||||
assert res.exit_code == 2
|
assert res.exit_code == 2
|
||||||
assert "Error: Use either --alias or --host, not both." in res.output
|
assert "Error: Use either --alias or --host, not both." in res.output
|
||||||
|
|
||||||
|
|
||||||
|
async def test_discover(discovery_mock, mocker):
|
||||||
|
"""Test discovery output."""
|
||||||
|
runner = CliRunner()
|
||||||
|
res = await runner.invoke(
|
||||||
|
cli,
|
||||||
|
[
|
||||||
|
"--discovery-timeout",
|
||||||
|
0,
|
||||||
|
"--username",
|
||||||
|
"foo",
|
||||||
|
"--password",
|
||||||
|
"bar",
|
||||||
|
"discover",
|
||||||
|
"--verbose",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert res.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_discover_unsupported(unsupported_device_info):
|
||||||
|
"""Test discovery output."""
|
||||||
|
runner = CliRunner()
|
||||||
|
res = await runner.invoke(
|
||||||
|
cli,
|
||||||
|
[
|
||||||
|
"--discovery-timeout",
|
||||||
|
0,
|
||||||
|
"--username",
|
||||||
|
"foo",
|
||||||
|
"--password",
|
||||||
|
"bar",
|
||||||
|
"discover",
|
||||||
|
"--verbose",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert res.exit_code == 0
|
||||||
|
assert "== Unsupported device ==" in res.output
|
||||||
|
assert "== Discovery Result ==" in res.output
|
||||||
|
|
||||||
|
|
||||||
|
async def test_host_unsupported(unsupported_device_info):
|
||||||
|
"""Test discovery output."""
|
||||||
|
runner = CliRunner()
|
||||||
|
host = "127.0.0.1"
|
||||||
|
|
||||||
|
res = await runner.invoke(
|
||||||
|
cli,
|
||||||
|
[
|
||||||
|
"--host",
|
||||||
|
host,
|
||||||
|
"--username",
|
||||||
|
"foo",
|
||||||
|
"--password",
|
||||||
|
"bar",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert res.exit_code != 0
|
||||||
|
assert isinstance(res.exception, UnsupportedDeviceException)
|
||||||
|
|
||||||
|
|
||||||
|
@new_discovery
|
||||||
|
async def test_discover_auth_failed(discovery_mock, mocker):
|
||||||
|
"""Test discovery output."""
|
||||||
|
runner = CliRunner()
|
||||||
|
host = "127.0.0.1"
|
||||||
|
discovery_mock.ip = host
|
||||||
|
device_class = Discover._get_device_class(discovery_mock.discovery_data)
|
||||||
|
mocker.patch.object(
|
||||||
|
device_class,
|
||||||
|
"update",
|
||||||
|
side_effect=AuthenticationException("Failed to authenticate"),
|
||||||
|
)
|
||||||
|
res = await runner.invoke(
|
||||||
|
cli,
|
||||||
|
[
|
||||||
|
"--discovery-timeout",
|
||||||
|
0,
|
||||||
|
"--username",
|
||||||
|
"foo",
|
||||||
|
"--password",
|
||||||
|
"bar",
|
||||||
|
"discover",
|
||||||
|
"--verbose",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert res.exit_code == 0
|
||||||
|
assert "== Authentication failed for device ==" in res.output
|
||||||
|
assert "== Discovery Result ==" in res.output
|
||||||
|
|
||||||
|
|
||||||
|
@new_discovery
|
||||||
|
async def test_host_auth_failed(discovery_mock, mocker):
|
||||||
|
"""Test discovery output."""
|
||||||
|
runner = CliRunner()
|
||||||
|
host = "127.0.0.1"
|
||||||
|
discovery_mock.ip = host
|
||||||
|
device_class = Discover._get_device_class(discovery_mock.discovery_data)
|
||||||
|
mocker.patch.object(
|
||||||
|
device_class,
|
||||||
|
"update",
|
||||||
|
side_effect=AuthenticationException("Failed to authenticate"),
|
||||||
|
)
|
||||||
|
res = await runner.invoke(
|
||||||
|
cli,
|
||||||
|
[
|
||||||
|
"--host",
|
||||||
|
host,
|
||||||
|
"--username",
|
||||||
|
"foo",
|
||||||
|
"--password",
|
||||||
|
"bar",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert res.exit_code != 0
|
||||||
|
assert isinstance(res.exception, AuthenticationException)
|
||||||
|
@ -202,7 +202,7 @@ async def test_discover_datagram_received(mocker, discovery_data):
|
|||||||
# Check that device in discovered_devices is initialized correctly
|
# Check that device in discovered_devices is initialized correctly
|
||||||
assert len(proto.discovered_devices) == 1
|
assert len(proto.discovered_devices) == 1
|
||||||
# Check that unsupported device is 1
|
# Check that unsupported device is 1
|
||||||
assert len(proto.unsupported_devices) == 1
|
assert len(proto.unsupported_device_exceptions) == 1
|
||||||
dev = proto.discovered_devices[addr]
|
dev = proto.discovered_devices[addr]
|
||||||
assert issubclass(dev.__class__, SmartDevice)
|
assert issubclass(dev.__class__, SmartDevice)
|
||||||
assert dev.host == addr
|
assert dev.host == addr
|
||||||
|
Loading…
Reference in New Issue
Block a user