4
4
5
5
import asyncio
6
6
from pprint import pformat as pf
7
+ from typing import TYPE_CHECKING , cast
7
8
8
9
import asyncclick as click
9
10
17
18
from kasa .discover import (
18
19
NEW_DISCOVERY_REDACTORS ,
19
20
ConnectAttempt ,
21
+ DeviceDict ,
20
22
DiscoveredRaw ,
21
23
DiscoveryResult ,
24
+ OnDiscoveredCallable ,
25
+ OnDiscoveredRawCallable ,
26
+ OnUnsupportedCallable ,
22
27
)
23
28
from kasa .iot .iotdevice import _extract_sys_info
24
29
from kasa .protocols .iotprotocol import REDACTORS as IOT_REDACTORS
30
35
31
36
@click .group (invoke_without_command = True )
32
37
@click .pass_context
33
- async def discover (ctx ):
38
+ async def discover (ctx : click . Context ):
34
39
"""Discover devices in the network."""
35
40
if ctx .invoked_subcommand is None :
36
41
return await ctx .invoke (detail )
37
42
38
43
44
+ @discover .result_callback ()
45
+ @click .pass_context
46
+ async def _close_protocols (ctx : click .Context , discovered : DeviceDict ):
47
+ """Close all the device protocols if discover was invoked directly by the user."""
48
+ if _discover_is_root_cmd (ctx ):
49
+ for dev in discovered .values ():
50
+ await dev .disconnect ()
51
+ return discovered
52
+
53
+
54
+ def _discover_is_root_cmd (ctx : click .Context ) -> bool :
55
+ """Will return true if discover was invoked directly by the user."""
56
+ root_ctx = ctx .find_root ()
57
+ return (
58
+ root_ctx .invoked_subcommand is None or root_ctx .invoked_subcommand == "discover"
59
+ )
60
+
61
+
39
62
@discover .command ()
40
63
@click .pass_context
41
- async def detail (ctx ) :
64
+ async def detail (ctx : click . Context ) -> DeviceDict :
42
65
"""Discover devices in the network using udp broadcasts."""
43
66
unsupported = []
44
67
auth_failed = []
@@ -59,10 +82,14 @@ async def print_unsupported(unsupported_exception: UnsupportedDeviceError) -> No
59
82
from .device import state
60
83
61
84
async def print_discovered (dev : Device ) -> None :
85
+ if TYPE_CHECKING :
86
+ assert ctx .parent
62
87
async with sem :
63
88
try :
64
89
await dev .update ()
65
90
except AuthenticationError :
91
+ if TYPE_CHECKING :
92
+ assert dev ._discovery_info
66
93
auth_failed .append (dev ._discovery_info )
67
94
echo ("== Authentication failed for device ==" )
68
95
_echo_discovery_info (dev ._discovery_info )
@@ -73,9 +100,11 @@ async def print_discovered(dev: Device) -> None:
73
100
echo ()
74
101
75
102
discovered = await _discover (
76
- ctx , print_discovered = print_discovered , print_unsupported = print_unsupported
103
+ ctx ,
104
+ print_discovered = print_discovered if _discover_is_root_cmd (ctx ) else None ,
105
+ print_unsupported = print_unsupported ,
77
106
)
78
- if ctx .parent . parent .params ["host" ]:
107
+ if ctx .find_root () .params ["host" ]:
79
108
return discovered
80
109
81
110
echo (f"Found { len (discovered )} devices" )
@@ -96,7 +125,7 @@ async def print_discovered(dev: Device) -> None:
96
125
help = "Set flag to redact sensitive data from raw output." ,
97
126
)
98
127
@click .pass_context
99
- async def raw (ctx , redact : bool ):
128
+ async def raw (ctx : click . Context , redact : bool ) -> DeviceDict :
100
129
"""Return raw discovery data returned from devices."""
101
130
102
131
def print_raw (discovered : DiscoveredRaw ):
@@ -116,7 +145,7 @@ def print_raw(discovered: DiscoveredRaw):
116
145
117
146
@discover .command ()
118
147
@click .pass_context
119
- async def list (ctx ) :
148
+ async def list (ctx : click . Context ) -> DeviceDict :
120
149
"""List devices in the network in a table using udp broadcasts."""
121
150
sem = asyncio .Semaphore ()
122
151
@@ -147,18 +176,24 @@ async def print_unsupported(unsupported_exception: UnsupportedDeviceError):
147
176
f"{ 'HOST' :<15} { 'MODEL' :<9} { 'DEVICE FAMILY' :<20} { 'ENCRYPT' :<7} "
148
177
f"{ 'HTTPS' :<5} { 'LV' :<3} { 'ALIAS' } "
149
178
)
150
- return await _discover (
179
+ discovered = await _discover (
151
180
ctx ,
152
181
print_discovered = print_discovered ,
153
182
print_unsupported = print_unsupported ,
154
183
do_echo = False ,
155
184
)
185
+ return discovered
156
186
157
187
158
188
async def _discover (
159
- ctx , * , print_discovered = None , print_unsupported = None , print_raw = None , do_echo = True
160
- ):
161
- params = ctx .parent .parent .params
189
+ ctx : click .Context ,
190
+ * ,
191
+ print_discovered : OnDiscoveredCallable | None = None ,
192
+ print_unsupported : OnUnsupportedCallable | None = None ,
193
+ print_raw : OnDiscoveredRawCallable | None = None ,
194
+ do_echo = True ,
195
+ ) -> DeviceDict :
196
+ params = ctx .find_root ().params
162
197
target = params ["target" ]
163
198
username = params ["username" ]
164
199
password = params ["password" ]
@@ -170,8 +205,9 @@ async def _discover(
170
205
credentials = Credentials (username , password ) if username and password else None
171
206
172
207
if host :
208
+ host = cast (str , host )
173
209
echo (f"Discovering device { host } for { discovery_timeout } seconds" )
174
- return await Discover .discover_single (
210
+ dev = await Discover .discover_single (
175
211
host ,
176
212
port = port ,
177
213
credentials = credentials ,
@@ -180,6 +216,12 @@ async def _discover(
180
216
on_unsupported = print_unsupported ,
181
217
on_discovered_raw = print_raw ,
182
218
)
219
+ if dev :
220
+ if print_discovered :
221
+ await print_discovered (dev )
222
+ return {host : dev }
223
+ else :
224
+ return {}
183
225
if do_echo :
184
226
echo (f"Discovering devices on { target } for { discovery_timeout } seconds" )
185
227
discovered_devices = await Discover .discover (
@@ -193,21 +235,18 @@ async def _discover(
193
235
on_discovered_raw = print_raw ,
194
236
)
195
237
196
- for device in discovered_devices .values ():
197
- await device .protocol .close ()
198
-
199
238
return discovered_devices
200
239
201
240
202
241
@discover .command ()
203
242
@click .pass_context
204
- async def config (ctx ) :
243
+ async def config (ctx : click . Context ) -> DeviceDict :
205
244
"""Bypass udp discovery and try to show connection config for a device.
206
245
207
246
Bypasses udp discovery and shows the parameters required to connect
208
247
directly to the device.
209
248
"""
210
- params = ctx .parent . parent .params
249
+ params = ctx .find_root () .params
211
250
username = params ["username" ]
212
251
password = params ["password" ]
213
252
timeout = params ["timeout" ]
@@ -239,6 +278,7 @@ def on_attempt(connect_attempt: ConnectAttempt, success: bool) -> None:
239
278
f"--encrypt-type { cparams .encryption_type .value } "
240
279
f"{ '--https' if cparams .https else '--no-https' } "
241
280
)
281
+ return {host : dev }
242
282
else :
243
283
error (f"Unable to connect to { host } " )
244
284
@@ -251,7 +291,7 @@ def _echo_dictionary(discovery_info: dict) -> None:
251
291
echo (f"\t { key_name_and_spaces } { value } " )
252
292
253
293
254
- def _echo_discovery_info (discovery_info ) -> None :
294
+ def _echo_discovery_info (discovery_info : dict ) -> None :
255
295
# We don't have discovery info when all connection params are passed manually
256
296
if discovery_info is None :
257
297
return
0 commit comments