FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Add support for alternative discovery protocol (20002/udp) (#488) · python-kasa/python-kasa@6055c29 · GitHub

Repository navigation

Commit 6055c29

Browse files
authored
Add support for alternative discovery protocol (20002/udp) (#488)
This will broadcast the new discovery message on the new port and log any responses received as unsupported devices.
1 parent 53021f0 commit 6055c29

4 files changed

Lines changed: 219 additions & 25 deletions

File tree

‎kasa/cli.py‎

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ def _device_to_serializable(val: SmartDevice):
103103
"--port",
104104
envvar="KASA_PORT",
105105
required=False,
106+
type=int,
106107
help="The port of the device to connect to.",
107108
)
108109
@click.option(
@@ -138,7 +139,17 @@ def _device_to_serializable(val: SmartDevice):
138139
)
139140
@click.version_option(package_name="python-kasa")
140141
@click.pass_context
141-
async def cli(ctx, host, port, alias, target, debug, type, json, discovery_timeout):
142+
async def cli(
143+
ctx,
144+
host,
145+
port,
146+
alias,
147+
target,
148+
debug,
149+
type,
150+
json,
151+
discovery_timeout,
152+
):
142153
"""A tool for controlling TP-Link smart home devices.""" # noqa
143154
# no need to perform any checks if we are just displaying the help
144155
if sys.argv[-1] == "--help":
@@ -238,13 +249,29 @@ async def join(dev: SmartDevice, ssid, password, keytype):
238249

239250
@cli.command()
240251
@click.option("--timeout", default=3, required=False)
252+
@click.option(
253+
"--show-unsupported",
254+
envvar="KASA_SHOW_UNSUPPORTED",
255+
required=False,
256+
default=False,
257+
is_flag=True,
258+
help="Print out discovered unsupported devices",
259+
)
241260
@click.pass_context
242-
async def discover(ctx, timeout):
261+
async def discover(ctx, timeout, show_unsupported):
243262
"""Discover devices in the network."""
244263
target = ctx.parent.params["target"]
245-
echo(f"Discovering devices on {target} for {timeout} seconds")
246264
sem = asyncio.Semaphore()
247265
discovered = dict()
266+
unsupported = []
267+
268+
async def print_unsupported(data: Dict):
269+
unsupported.append(data)
270+
if show_unsupported:
271+
echo(f"Found unsupported device (tapo/unknown encryption): {data}")
272+
echo()
273+
274+
echo(f"Discovering devices on {target} for {timeout} seconds")
248275

249276
async def print_discovered(dev: SmartDevice):
250277
await dev.update()
@@ -255,9 +282,23 @@ async def print_discovered(dev: SmartDevice):
255282
echo()
256283

257284
await Discover.discover(
258-
target=target, timeout=timeout, on_discovered=print_discovered
285+
target=target,
286+
timeout=timeout,
287+
on_discovered=print_discovered,
288+
on_unsupported=print_unsupported,
259289
)
260290

291+
echo(f"Found {len(discovered)} devices")
292+
if unsupported:
293+
echo(
294+
f"Found {len(unsupported)} unsupported devices"
295+
+ (
296+
""
297+
if show_unsupported
298+
else ", to show them use: kasa discover --show-unsupported"
299+
)
300+
)
301+
261302
return discovered
262303

263304

‎kasa/discover.py‎

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
"""Discovery module for TP-Link Smart Home devices."""
22
import asyncio
3+
import binascii
34
import logging
45
import socket
56
from typing import Awaitable, Callable, Dict, Optional, Type, cast
67

8+
# When support for cpython older than 3.11 is dropped
9+
# async_timeout can be replaced with asyncio.timeout
10+
from async_timeout import timeout as asyncio_timeout
11+
12+
from kasa.exceptions import UnsupportedDeviceException
713
from kasa.json import dumps as json_dumps
814
from kasa.json import loads as json_loads
915
from kasa.protocol import TPLinkSmartHomeProtocol
@@ -36,13 +42,22 @@ def __init__(
3642
target: str = "255.255.255.255",
3743
discovery_packets: int = 3,
3844
interface: Optional[str] = None,
45+
on_unsupported: Optional[Callable[[Dict], Awaitable[None]]] = None,
46+
port: Optional[int] = None,
47+
discovered_event: Optional[asyncio.Event] = None,
3948
):
4049
self.transport = None
4150
self.discovery_packets = discovery_packets
4251
self.interface = interface
4352
self.on_discovered = on_discovered
44-
self.target = (target, Discover.DISCOVERY_PORT)
53+
self.discovery_port = port or Discover.DISCOVERY_PORT
54+
self.target = (target, self.discovery_port)
55+
self.target_2 = (target, Discover.DISCOVERY_PORT_2)
4556
self.discovered_devices = {}
57+
self.unsupported_devices: Dict = {}
58+
self.invalid_device_exceptions: Dict = {}
59+
self.on_unsupported = on_unsupported
60+
self.discovered_event = discovered_event
4661

4762
def connection_made(self, transport) -> None:
4863
"""Set socket options for broadcasting."""
@@ -69,30 +84,58 @@ def do_discover(self) -> None:
6984
encrypted_req = TPLinkSmartHomeProtocol.encrypt(req)
7085
for i in range(self.discovery_packets):
7186
self.transport.sendto(encrypted_req[4:], self.target) # type: ignore
87+
self.transport.sendto(Discover.DISCOVERY_QUERY_2, self.target_2) # type: ignore
7288

7389
def datagram_received(self, data, addr) -> None:
7490
"""Handle discovery responses."""
7591
ip, port = addr
76-
if ip in self.discovered_devices:
92+
if (
93+
ip in self.discovered_devices
94+
or ip in self.unsupported_devices
95+
or ip in self.invalid_device_exceptions
96+
):
7797
return
7898

79-
info = json_loads(TPLinkSmartHomeProtocol.decrypt(data))
80-
_LOGGER.debug("[DISCOVERY] %s << %s", ip, info)
99+
if port == self.discovery_port:
100+
info = json_loads(TPLinkSmartHomeProtocol.decrypt(data))
101+
_LOGGER.debug("[DISCOVERY] %s << %s", ip, info)
102+
103+
elif port == Discover.DISCOVERY_PORT_2:
104+
info = json_loads(data[16:])
105+
self.unsupported_devices[ip] = info
106+
if self.on_unsupported is not None:
107+
asyncio.ensure_future(self.on_unsupported(info))
108+
_LOGGER.debug("[DISCOVERY] Unsupported device found at %s << %s", ip, info)
109+
if self.discovered_event is not None and "255" not in self.target[0].split(
110+
"."
111+
):
112+
self.discovered_event.set()
113+
return
81114

82115
try:
83116
device_class = Discover._get_device_class(info)
84117
except SmartDeviceException as ex:
85-
_LOGGER.debug("Unable to find device type from %s: %s", info, ex)
118+
_LOGGER.debug(
119+
"[DISCOVERY] Unable to find device type from %s: %s", info, ex
120+
)
121+
self.invalid_device_exceptions[ip] = ex
122+
if self.discovered_event is not None and "255" not in self.target[0].split(
123+
"."
124+
):
125+
self.discovered_event.set()
86126
return
87127

88-
device = device_class(ip)
128+
device = device_class(ip, port=port)
89129
device.update_from_discover_info(info)
90130

91131
self.discovered_devices[ip] = device
92132

93133
if self.on_discovered is not None:
94134
asyncio.ensure_future(self.on_discovered(device))
95135

136+
if self.discovered_event is not None and "255" not in self.target[0].split("."):
137+
self.discovered_event.set()
138+
96139
def error_received(self, ex):
97140
"""Handle asyncio.Protocol errors."""
98141
_LOGGER.error("Got error: %s", ex)
@@ -142,6 +185,9 @@ class Discover:
142185
"system": {"get_sysinfo": None},
143186
}
144187

188+
DISCOVERY_PORT_2 = 20002
189+
DISCOVERY_QUERY_2 = binascii.unhexlify("020000010000000000000000463cb5d3")
190+
145191
@staticmethod
146192
async def discover(
147193
*,
@@ -150,6 +196,7 @@ async def discover(
150196
timeout=5,
151197
discovery_packets=3,
152198
interface=None,
199+
on_unsupported=None,
153200
) -> DeviceDict:
154201
"""Discover supported devices.
155202
@@ -177,6 +224,7 @@ async def discover(
177224
on_discovered=on_discovered,
178225
discovery_packets=discovery_packets,
179226
interface=interface,
227+
on_unsupported=on_unsupported,
180228
),
181229
local_addr=("0.0.0.0", 0),
182230
)
@@ -193,22 +241,47 @@ async def discover(
193241
return protocol.discovered_devices
194242

195243
@staticmethod
196-
async def discover_single(host: str, *, port: Optional[int] = None) -> SmartDevice:
244+
async def discover_single(
245+
host: str, *, port: Optional[int] = None, timeout=5
246+
) -> SmartDevice:
197247
"""Discover a single device by the given IP address.
198248
199249
:param host: Hostname of device to query
200250
:rtype: SmartDevice
201251
:return: Object for querying/controlling found device.
202252
"""
203-
protocol = TPLinkSmartHomeProtocol(host, port=port)
253+
loop = asyncio.get_event_loop()
254+
event = asyncio.Event()
255+
transport, protocol = await loop.create_datagram_endpoint(
256+
lambda: _DiscoverProtocol(target=host, port=port, discovered_event=event),
257+
local_addr=("0.0.0.0", 0),
258+
)
259+
protocol = cast(_DiscoverProtocol, protocol)
204260

205-
info = await protocol.query(Discover.DISCOVERY_QUERY)
261+
try:
262+
_LOGGER.debug("Waiting a total of %s seconds for responses...", timeout)
206263

207-
device_class = Discover._get_device_class(info)
208-
dev = device_class(host, port=port)
209-
await dev.update()
264+
async with asyncio_timeout(timeout):
265+
await event.wait()
266+
except asyncio.TimeoutError:
267+
raise SmartDeviceException(
268+
f"Timed out getting discovery response for {host}"
269+
)
270+
finally:
271+
transport.close()
210272

211-
return dev
273+
if host in protocol.discovered_devices:
274+
dev = protocol.discovered_devices[host]
275+
await dev.update()
276+
return dev
277+
elif host in protocol.unsupported_devices:
278+
raise UnsupportedDeviceException(
279+
f"Unsupported device {host}: {protocol.unsupported_devices[host]}"
280+
)
281+
elif host in protocol.invalid_device_exceptions:
282+
raise protocol.invalid_device_exceptions[host]
283+
else:
284+
raise SmartDeviceException(f"Unable to get discovery response for {host}")
212285

213286
@staticmethod
214287
def _get_device_class(info: dict) -> Type[SmartDevice]:

‎kasa/exceptions.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@
33

44
class SmartDeviceException(Exception):
55
"""Base exception for device errors."""
6+
7+
8+
class UnsupportedDeviceException(SmartDeviceException):
9+
"""Exception for trying to connect to unsupported devices."""

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL