Merge branch 'master' into connect_single_device_type

This commit is contained in:
J. Nick Koston 2023-11-19 09:44:57 -06:00 committed by GitHub
commit 8ca64177e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 72 additions and 9 deletions

View File

@ -184,15 +184,24 @@ If your device is unlisted but working, please open a pull request to update the
## Resources ## 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) * [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) * [TP-Link Smart Home Device Simulator](https://github.com/plasticrake/tplink-smarthome-simulator)
* [Unofficial API documentation](https://github.com/plasticrake/tplink-smarthome-api) * [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) * [MQTT access to TP-Link devices, using python-kasa](https://github.com/flavio-fernandes/mqtt2kasa)
### TP-Link Tapo support ### 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) * [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) * [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)

View File

@ -1,6 +1,7 @@
"""Discovery module for TP-Link Smart Home devices.""" """Discovery module for TP-Link Smart Home devices."""
import asyncio import asyncio
import binascii import binascii
import ipaddress
import logging import logging
import socket import socket
from typing import Awaitable, Callable, Dict, Optional, Type, cast from typing import Awaitable, Callable, Dict, Optional, Type, cast
@ -281,9 +282,34 @@ class Discover:
""" """
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
event = asyncio.Event() 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( transport, protocol = await loop.create_datagram_endpoint(
lambda: _DiscoverProtocol( lambda: _DiscoverProtocol(
target=host, target=ip,
port=port, port=port,
discovered_event=event, discovered_event=event,
credentials=credentials, credentials=credentials,
@ -305,16 +331,17 @@ class Discover:
finally: finally:
transport.close() transport.close()
if host in protocol.discovered_devices: if ip in protocol.discovered_devices:
dev = protocol.discovered_devices[host] dev = protocol.discovered_devices[ip]
dev.host = host
await dev.update() await dev.update()
return dev return dev
elif host in protocol.unsupported_devices: elif ip in protocol.unsupported_devices:
raise UnsupportedDeviceException( 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: elif ip in protocol.invalid_device_exceptions:
raise protocol.invalid_device_exceptions[host] raise protocol.invalid_device_exceptions[ip]
else: else:
raise SmartDeviceException(f"Unable to get discovery response for {host}") raise SmartDeviceException(f"Unable to get discovery response for {host}")

View File

@ -1,6 +1,8 @@
# type: ignore # type: ignore
import re import re
from typing import Type from typing import Type
import socket
import sys
import pytest # type: ignore # https://github.com/pytest-dev/pytest/issues/3342 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 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]) @pytest.mark.parametrize("custom_port", [123, None])
async def test_connect_single(discovery_data: dict, mocker, custom_port): async def test_connect_single(discovery_data: dict, mocker, custom_port):
"""Make sure that connect_single returns an initialized SmartDevice instance.""" """Make sure that connect_single returns an initialized SmartDevice instance."""