diff --git a/README.md b/README.md index 97525a28..f6574778 100644 --- a/README.md +++ b/README.md @@ -184,15 +184,24 @@ If your device is unlisted but working, please open a pull request to update the ## Resources -### Links +### Developer Resources -* [pyHS100](https://github.com/GadgetReactor/pyHS100) provides synchronous interface and is the unmaintained predecessor of this library. * [softScheck's github contains lot of information and wireshark dissector](https://github.com/softScheck/tplink-smartplug#wireshark-dissector) * [TP-Link Smart Home Device Simulator](https://github.com/plasticrake/tplink-smarthome-simulator) * [Unofficial API documentation](https://github.com/plasticrake/tplink-smarthome-api) +* [Another unofficial API documentation](https://github.com/whitslack/kasa) +* [pyHS100](https://github.com/GadgetReactor/pyHS100) provides synchronous interface and is the unmaintained predecessor of this library. + + +### Library Users + +* [Home Assistant](https://www.home-assistant.io/integrations/tplink/) * [MQTT access to TP-Link devices, using python-kasa](https://github.com/flavio-fernandes/mqtt2kasa) ### TP-Link Tapo support +* [PyTapo - Python library for communication with Tapo Cameras](https://github.com/JurajNyiri/pytapo) * [Tapo P100 (Tapo P105/P100 plugs, Tapo L510E bulbs)](https://github.com/fishbigger/TapoP100) * [Home Assistant integration](https://github.com/fishbigger/HomeAssistant-Tapo-P100-Control) +* [plugp100, another tapo library](https://github.com/petretiandrea/plugp100) + * [Home Assistant integration](https://github.com/petretiandrea/home-assistant-tapo-p100) diff --git a/kasa/discover.py b/kasa/discover.py index 0dc7cee8..db7235b1 100755 --- a/kasa/discover.py +++ b/kasa/discover.py @@ -1,6 +1,7 @@ """Discovery module for TP-Link Smart Home devices.""" import asyncio import binascii +import ipaddress import logging import socket from typing import Awaitable, Callable, Dict, Optional, Type, cast @@ -281,9 +282,34 @@ class Discover: """ loop = asyncio.get_event_loop() event = asyncio.Event() + + try: + ipaddress.ip_address(host) + ip = host + except ValueError: + try: + adrrinfo = await loop.getaddrinfo( + host, + 0, + type=socket.SOCK_DGRAM, + family=socket.AF_INET, + ) + # getaddrinfo returns a list of 5 tuples with the following structure: + # (family, type, proto, canonname, sockaddr) + # where sockaddr is 2 tuple (ip, port). + # hence [0][4][0] is a stable array access because if no socket + # address matches the host for SOCK_DGRAM AF_INET the gaierror + # would be raised. + # https://docs.python.org/3/library/socket.html#socket.getaddrinfo + ip = adrrinfo[0][4][0] + except socket.gaierror as gex: + raise SmartDeviceException( + f"Could not resolve hostname {host}" + ) from gex + transport, protocol = await loop.create_datagram_endpoint( lambda: _DiscoverProtocol( - target=host, + target=ip, port=port, discovered_event=event, credentials=credentials, @@ -305,16 +331,17 @@ class Discover: finally: transport.close() - if host in protocol.discovered_devices: - dev = protocol.discovered_devices[host] + if ip in protocol.discovered_devices: + dev = protocol.discovered_devices[ip] + dev.host = host await dev.update() return dev - elif host in protocol.unsupported_devices: + elif ip in protocol.unsupported_devices: raise UnsupportedDeviceException( - f"Unsupported device {host}: {protocol.unsupported_devices[host]}" + f"Unsupported device {host}: {protocol.unsupported_devices[ip]}" ) - elif host in protocol.invalid_device_exceptions: - raise protocol.invalid_device_exceptions[host] + elif ip in protocol.invalid_device_exceptions: + raise protocol.invalid_device_exceptions[ip] else: raise SmartDeviceException(f"Unable to get discovery response for {host}") diff --git a/kasa/tests/test_discovery.py b/kasa/tests/test_discovery.py index f3b2630a..ff427837 100644 --- a/kasa/tests/test_discovery.py +++ b/kasa/tests/test_discovery.py @@ -1,6 +1,8 @@ # type: ignore import re from typing import Type +import socket +import sys import pytest # type: ignore # https://github.com/pytest-dev/pytest/issues/3342 @@ -84,6 +86,31 @@ async def test_discover_single(discovery_data: dict, mocker, custom_port): assert x.port == custom_port or x.port == 9999 +async def test_discover_single_hostname(discovery_data: dict, mocker): + """Make sure that discover_single returns an initialized SmartDevice instance.""" + host = "foobar" + ip = "127.0.0.1" + + def mock_discover(self): + self.datagram_received( + protocol.TPLinkSmartHomeProtocol.encrypt(json_dumps(discovery_data))[4:], + (ip, 9999), + ) + + mocker.patch.object(_DiscoverProtocol, "do_discover", mock_discover) + mocker.patch("kasa.TPLinkSmartHomeProtocol.query", return_value=discovery_data) + mocker.patch("socket.getaddrinfo", return_value=[(None, None, None, None, (ip, 0))]) + + x = await Discover.discover_single(host) + assert issubclass(x.__class__, SmartDevice) + assert x._sys_info is not None + assert x.host == host + + mocker.patch("socket.getaddrinfo", side_effect=socket.gaierror()) + with pytest.raises(SmartDeviceException): + x = await Discover.discover_single(host) + + @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."""