Skip to content

Commit 8ca6417

Browse files
authored
Merge branch 'master' into connect_single_device_type
2 parents bcdcd5f + bde07d1 commit 8ca6417

File tree

3 files changed

+72
-9
lines changed

3 files changed

+72
-9
lines changed

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,15 +184,24 @@ If your device is unlisted but working, please open a pull request to update the
184184

185185
## Resources
186186

187-
### Links
187+
### Developer Resources
188188

189-
* [pyHS100](https://github.com/GadgetReactor/pyHS100) provides synchronous interface and is the unmaintained predecessor of this library.
190189
* [softScheck's github contains lot of information and wireshark dissector](https://github.com/softScheck/tplink-smartplug#wireshark-dissector)
191190
* [TP-Link Smart Home Device Simulator](https://github.com/plasticrake/tplink-smarthome-simulator)
192191
* [Unofficial API documentation](https://github.com/plasticrake/tplink-smarthome-api)
192+
* [Another unofficial API documentation](https://github.com/whitslack/kasa)
193+
* [pyHS100](https://github.com/GadgetReactor/pyHS100) provides synchronous interface and is the unmaintained predecessor of this library.
194+
195+
196+
### Library Users
197+
198+
* [Home Assistant](https://www.home-assistant.io/integrations/tplink/)
193199
* [MQTT access to TP-Link devices, using python-kasa](https://github.com/flavio-fernandes/mqtt2kasa)
194200

195201
### TP-Link Tapo support
196202

203+
* [PyTapo - Python library for communication with Tapo Cameras](https://github.com/JurajNyiri/pytapo)
197204
* [Tapo P100 (Tapo P105/P100 plugs, Tapo L510E bulbs)](https://github.com/fishbigger/TapoP100)
198205
* [Home Assistant integration](https://github.com/fishbigger/HomeAssistant-Tapo-P100-Control)
206+
* [plugp100, another tapo library](https://github.com/petretiandrea/plugp100)
207+
* [Home Assistant integration](https://github.com/petretiandrea/home-assistant-tapo-p100)

kasa/discover.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Discovery module for TP-Link Smart Home devices."""
22
import asyncio
33
import binascii
4+
import ipaddress
45
import logging
56
import socket
67
from typing import Awaitable, Callable, Dict, Optional, Type, cast
@@ -281,9 +282,34 @@ async def discover_single(
281282
"""
282283
loop = asyncio.get_event_loop()
283284
event = asyncio.Event()
285+
286+
try:
287+
ipaddress.ip_address(host)
288+
ip = host
289+
except ValueError:
290+
try:
291+
adrrinfo = await loop.getaddrinfo(
292+
host,
293+
0,
294+
type=socket.SOCK_DGRAM,
295+
family=socket.AF_INET,
296+
)
297+
# getaddrinfo returns a list of 5 tuples with the following structure:
298+
# (family, type, proto, canonname, sockaddr)
299+
# where sockaddr is 2 tuple (ip, port).
300+
# hence [0][4][0] is a stable array access because if no socket
301+
# address matches the host for SOCK_DGRAM AF_INET the gaierror
302+
# would be raised.
303+
# https://docs.python.org/3/library/socket.html#socket.getaddrinfo
304+
ip = adrrinfo[0][4][0]
305+
except socket.gaierror as gex:
306+
raise SmartDeviceException(
307+
f"Could not resolve hostname {host}"
308+
) from gex
309+
284310
transport, protocol = await loop.create_datagram_endpoint(
285311
lambda: _DiscoverProtocol(
286-
target=host,
312+
target=ip,
287313
port=port,
288314
discovered_event=event,
289315
credentials=credentials,
@@ -305,16 +331,17 @@ async def discover_single(
305331
finally:
306332
transport.close()
307333

308-
if host in protocol.discovered_devices:
309-
dev = protocol.discovered_devices[host]
334+
if ip in protocol.discovered_devices:
335+
dev = protocol.discovered_devices[ip]
336+
dev.host = host
310337
await dev.update()
311338
return dev
312-
elif host in protocol.unsupported_devices:
339+
elif ip in protocol.unsupported_devices:
313340
raise UnsupportedDeviceException(
314-
f"Unsupported device {host}: {protocol.unsupported_devices[host]}"
341+
f"Unsupported device {host}: {protocol.unsupported_devices[ip]}"
315342
)
316-
elif host in protocol.invalid_device_exceptions:
317-
raise protocol.invalid_device_exceptions[host]
343+
elif ip in protocol.invalid_device_exceptions:
344+
raise protocol.invalid_device_exceptions[ip]
318345
else:
319346
raise SmartDeviceException(f"Unable to get discovery response for {host}")
320347

kasa/tests/test_discovery.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# type: ignore
22
import re
33
from typing import Type
4+
import socket
5+
import sys
46

57
import pytest # type: ignore # https://github.com/pytest-dev/pytest/issues/3342
68

@@ -84,6 +86,31 @@ def mock_discover(self):
8486
assert x.port == custom_port or x.port == 9999
8587

8688

89+
async def test_discover_single_hostname(discovery_data: dict, mocker):
90+
"""Make sure that discover_single returns an initialized SmartDevice instance."""
91+
host = "foobar"
92+
ip = "127.0.0.1"
93+
94+
def mock_discover(self):
95+
self.datagram_received(
96+
protocol.TPLinkSmartHomeProtocol.encrypt(json_dumps(discovery_data))[4:],
97+
(ip, 9999),
98+
)
99+
100+
mocker.patch.object(_DiscoverProtocol, "do_discover", mock_discover)
101+
mocker.patch("kasa.TPLinkSmartHomeProtocol.query", return_value=discovery_data)
102+
mocker.patch("socket.getaddrinfo", return_value=[(None, None, None, None, (ip, 0))])
103+
104+
x = await Discover.discover_single(host)
105+
assert issubclass(x.__class__, SmartDevice)
106+
assert x._sys_info is not None
107+
assert x.host == host
108+
109+
mocker.patch("socket.getaddrinfo", side_effect=socket.gaierror())
110+
with pytest.raises(SmartDeviceException):
111+
x = await Discover.discover_single(host)
112+
113+
87114
@pytest.mark.parametrize("custom_port", [123, None])
88115
async def test_connect_single(discovery_data: dict, mocker, custom_port):
89116
"""Make sure that connect_single returns an initialized SmartDevice instance."""

0 commit comments

Comments
 (0)