Add a connect_single method to Discover to avoid the need for UDP (#528)

This should equate to a significant reliability improvement for networks with poor wifi (edge of range)/udp.
This commit is contained in:
J. Nick Koston 2023-10-07 14:29:22 -10:00 committed by GitHub
parent 528f5e9e07
commit 85c8410c3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 62 additions and 0 deletions

View File

@ -256,6 +256,11 @@ class Discover:
) -> SmartDevice: ) -> SmartDevice:
"""Discover a single device by the given IP address. """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 :param host: Hostname of device to query
:rtype: SmartDevice :rtype: SmartDevice
:return: Object for querying/controlling found device. :return: Object for querying/controlling found device.
@ -299,6 +304,43 @@ class Discover:
else: else:
raise SmartDeviceException(f"Unable to get discovery response for {host}") 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 @staticmethod
def _get_device_class(info: dict) -> Type[SmartDevice]: def _get_device_class(info: dict) -> Type[SmartDevice]:
"""Find SmartDevice subclass for device described by passed data.""" """Find SmartDevice subclass for device described by passed data."""

View File

@ -74,6 +74,26 @@ async def test_discover_single(discovery_data: dict, mocker, custom_port):
assert x.port == custom_port or 9999 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 = { UNSUPPORTED = {
"result": { "result": {
"device_id": "xx", "device_id": "xx",