diff --git a/kasa/discover.py b/kasa/discover.py index a39b2790..4cb7a532 100755 --- a/kasa/discover.py +++ b/kasa/discover.py @@ -256,6 +256,11 @@ class Discover: ) -> 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. @@ -299,6 +304,43 @@ class Discover: 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.""" diff --git a/kasa/tests/test_discovery.py b/kasa/tests/test_discovery.py index 41578a2c..2aa10f1c 100644 --- a/kasa/tests/test_discovery.py +++ b/kasa/tests/test_discovery.py @@ -74,6 +74,26 @@ async def test_discover_single(discovery_data: dict, mocker, custom_port): 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",