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
42 changes: 42 additions & 0 deletions kasa/discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,11 @@ async def discover_single(
) -> SmartDevice:
"""Discover a single device by the given IP address.

It is generally preferred to avoid :func:`discover_single()` and
use :func:`connect_single()` instead as it should perform better when
the WiFi network is congested or the device is not responding
to discovery requests.

:param host: Hostname of device to query
:rtype: SmartDevice
:return: Object for querying/controlling found device.
Expand Down Expand Up @@ -299,6 +304,43 @@ async def discover_single(
else:
raise SmartDeviceException(f"Unable to get discovery response for {host}")

@staticmethod
async def connect_single(
host: str,
*,
port: Optional[int] = None,
timeout=5,
credentials: Optional[Credentials] = None,
) -> SmartDevice:
"""Connect to a single device by the given IP address.

This method avoids the UDP based discovery process and
will connect directly to the device to query its type.

It is generally preferred to avoid :func:`discover_single()` and
use this function instead as it should perform better when
the WiFi network is congested or the device is not responding
to discovery requests.

The device type is discovered by querying the device.

:param host: Hostname of device to query
:rtype: SmartDevice
:return: Object for querying/controlling found device.
"""
unknown_dev = SmartDevice(
host=host, port=port, credentials=credentials, timeout=timeout
)
await unknown_dev.update()
device_class = Discover._get_device_class(unknown_dev.internal_state)
dev = device_class(
host=host, port=port, credentials=credentials, timeout=timeout
)
# Reuse the connection from the unknown device
# so we don't have to reconnect
dev.protocol = unknown_dev.protocol
return dev

@staticmethod
def _get_device_class(info: dict) -> Type[SmartDevice]:
"""Find SmartDevice subclass for device described by passed data."""
Expand Down
20 changes: 20 additions & 0 deletions kasa/tests/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,26 @@ def mock_discover(self):
assert x.port == custom_port or 9999


@pytest.mark.parametrize("custom_port", [123, None])
async def test_connect_single(discovery_data: dict, mocker, custom_port):
"""Make sure that connect_single returns an initialized SmartDevice instance."""
host = "127.0.0.1"
mocker.patch("kasa.TPLinkSmartHomeProtocol.query", return_value=discovery_data)

dev = await Discover.connect_single(host, port=custom_port)
assert issubclass(dev.__class__, SmartDevice)
assert dev.port == custom_port or 9999


async def test_connect_single_query_fails(discovery_data: dict, mocker):
"""Make sure that connect_single fails when query fails."""
host = "127.0.0.1"
mocker.patch("kasa.TPLinkSmartHomeProtocol.query", side_effect=SmartDeviceException)

with pytest.raises(SmartDeviceException):
await Discover.connect_single(host)


UNSUPPORTED = {
"result": {
"device_id": "xx",
Expand Down