|
32 | 32 | from kasa.discover import DiscoveryResult |
33 | 33 | from kasa.exceptions import SmartErrorCode |
34 | 34 | from kasa.smart import SmartDevice |
| 35 | +from kasa.smartprotocol import _ChildProtocolWrapper |
35 | 36 |
|
36 | 37 | Call = namedtuple("Call", "module method") |
37 | | -SmartCall = namedtuple("SmartCall", "module request should_succeed") |
| 38 | +SmartCall = namedtuple("SmartCall", "module request should_succeed child_device_id") |
38 | 39 |
|
39 | 40 | _LOGGER = logging.getLogger(__name__) |
40 | 41 |
|
@@ -69,6 +70,10 @@ def scrub(res): |
69 | 70 | "parent_device_id", # for hub children |
70 | 71 | "setup_code", # matter |
71 | 72 | "setup_payload", # matter |
| 73 | + "mfi_setup_code", # mfi_ for homekit |
| 74 | + "mfi_setup_id", |
| 75 | + "mfi_token_token", |
| 76 | + "mfi_token_uuid", |
72 | 77 | ] |
73 | 78 |
|
74 | 79 | for k, v in res.items(): |
@@ -105,6 +110,8 @@ def scrub(res): |
105 | 110 | v = "#MASKED_NAME#" |
106 | 111 | elif isinstance(res[k], int): |
107 | 112 | v = 0 |
| 113 | + elif k == "device_id" and "SCRUBBED" in v: |
| 114 | + pass # already scrubbed |
108 | 115 | elif k == "device_id" and len(v) > 40: |
109 | 116 | # retain the last two chars when scrubbing child ids |
110 | 117 | end = v[-2:] |
@@ -289,8 +296,15 @@ async def _make_requests_or_exit( |
289 | 296 | requests: List[SmartRequest], |
290 | 297 | name: str, |
291 | 298 | batch_size: int, |
| 299 | + *, |
| 300 | + child_device_id: str, |
292 | 301 | ) -> Dict[str, Dict]: |
293 | 302 | final = {} |
| 303 | + protocol = ( |
| 304 | + device.protocol |
| 305 | + if child_device_id == 0 |
| 306 | + else _ChildProtocolWrapper(child_device_id, device.protocol) |
| 307 | + ) |
294 | 308 | try: |
295 | 309 | end = len(requests) |
296 | 310 | step = batch_size # Break the requests down as there seems to be a size limit |
@@ -300,9 +314,7 @@ async def _make_requests_or_exit( |
300 | 314 | request: Union[List[SmartRequest], SmartRequest] = ( |
301 | 315 | requests_step[0] if len(requests_step) == 1 else requests_step |
302 | 316 | ) |
303 | | - responses = await device.protocol.query( |
304 | | - SmartRequest._create_request_dict(request) |
305 | | - ) |
| 317 | + responses = await protocol.query(SmartRequest._create_request_dict(request)) |
306 | 318 | for method, result in responses.items(): |
307 | 319 | final[method] = result |
308 | 320 | return final |
@@ -331,38 +343,36 @@ async def _make_requests_or_exit( |
331 | 343 | await device.protocol.close() |
332 | 344 |
|
333 | 345 |
|
334 | | -async def get_smart_fixture(device: SmartDevice, batch_size: int): |
335 | | - """Get fixture for new TAPO style protocol.""" |
| 346 | +async def get_smart_test_calls(device: SmartDevice): |
| 347 | + """Get the list of test calls to make.""" |
| 348 | + test_calls = [] |
| 349 | + successes = [] |
| 350 | + child_device_components = {} |
| 351 | + |
336 | 352 | extra_test_calls = [ |
337 | 353 | SmartCall( |
338 | 354 | module="temp_humidity_records", |
339 | 355 | request=SmartRequest.get_raw_request("get_temp_humidity_records"), |
340 | 356 | should_succeed=False, |
341 | | - ), |
342 | | - SmartCall( |
343 | | - module="child_device_list", |
344 | | - request=SmartRequest.get_raw_request("get_child_device_list"), |
345 | | - should_succeed=False, |
346 | | - ), |
347 | | - SmartCall( |
348 | | - module="child_device_component_list", |
349 | | - request=SmartRequest.get_raw_request("get_child_device_component_list"), |
350 | | - should_succeed=False, |
| 357 | + child_device_id="", |
351 | 358 | ), |
352 | 359 | SmartCall( |
353 | 360 | module="trigger_logs", |
354 | 361 | request=SmartRequest.get_raw_request( |
355 | 362 | "get_trigger_logs", SmartRequest.GetTriggerLogsParams(5, 0) |
356 | 363 | ), |
357 | 364 | should_succeed=False, |
| 365 | + child_device_id="", |
358 | 366 | ), |
359 | 367 | ] |
360 | 368 |
|
361 | | - successes = [] |
362 | | - |
363 | 369 | click.echo("Testing component_nego call ..", nl=False) |
364 | 370 | responses = await _make_requests_or_exit( |
365 | | - device, [SmartRequest.component_nego()], "component_nego call", batch_size |
| 371 | + device, |
| 372 | + [SmartRequest.component_nego()], |
| 373 | + "component_nego call", |
| 374 | + batch_size=1, |
| 375 | + child_device_id="", |
366 | 376 | ) |
367 | 377 | component_info_response = responses["component_nego"] |
368 | 378 | click.echo(click.style("OK", fg="green")) |
@@ -371,35 +381,109 @@ async def get_smart_fixture(device: SmartDevice, batch_size: int): |
371 | 381 | module="component_nego", |
372 | 382 | request=SmartRequest("component_nego"), |
373 | 383 | should_succeed=True, |
| 384 | + child_device_id="", |
374 | 385 | ) |
375 | 386 | ) |
376 | | - |
377 | | - test_calls = [] |
378 | | - should_succeed = [] |
379 | | - |
380 | | - for item in component_info_response["component_list"]: |
381 | | - component_id = item["id"] |
382 | | - ver_code = item["ver_code"] |
| 387 | + components = { |
| 388 | + item["id"]: item["ver_code"] |
| 389 | + for item in component_info_response["component_list"] |
| 390 | + } |
| 391 | + |
| 392 | + if "child_device" in components: |
| 393 | + child_components = await _make_requests_or_exit( |
| 394 | + device, |
| 395 | + [SmartRequest.get_child_device_component_list()], |
| 396 | + "child device component list", |
| 397 | + batch_size=1, |
| 398 | + child_device_id="", |
| 399 | + ) |
| 400 | + successes.append( |
| 401 | + SmartCall( |
| 402 | + module="child_component_list", |
| 403 | + request=SmartRequest.get_child_device_component_list(), |
| 404 | + should_succeed=True, |
| 405 | + child_device_id="", |
| 406 | + ) |
| 407 | + ) |
| 408 | + test_calls.append( |
| 409 | + SmartCall( |
| 410 | + module="child_device_list", |
| 411 | + request=SmartRequest.get_child_device_list(), |
| 412 | + should_succeed=True, |
| 413 | + child_device_id="", |
| 414 | + ) |
| 415 | + ) |
| 416 | + # Get list of child components to call |
| 417 | + if "control_child" in components: |
| 418 | + child_device_components = { |
| 419 | + child_component_list["device_id"]: { |
| 420 | + item["id"]: item["ver_code"] |
| 421 | + for item in child_component_list["component_list"] |
| 422 | + } |
| 423 | + for child_component_list in child_components[ |
| 424 | + "get_child_device_component_list" |
| 425 | + ]["child_component_list"] |
| 426 | + } |
| 427 | + |
| 428 | + # Get component calls |
| 429 | + for component_id, ver_code in components.items(): |
| 430 | + if component_id == "child_device": |
| 431 | + continue |
383 | 432 | if (requests := get_component_requests(component_id, ver_code)) is not None: |
384 | 433 | component_test_calls = [ |
385 | | - SmartCall(module=component_id, request=request, should_succeed=True) |
| 434 | + SmartCall( |
| 435 | + module=component_id, |
| 436 | + request=request, |
| 437 | + should_succeed=True, |
| 438 | + child_device_id="", |
| 439 | + ) |
386 | 440 | for request in requests |
387 | 441 | ] |
388 | 442 | test_calls.extend(component_test_calls) |
389 | | - should_succeed.extend(component_test_calls) |
390 | 443 | else: |
391 | 444 | click.echo(f"Skipping {component_id}..", nl=False) |
392 | 445 | click.echo(click.style("UNSUPPORTED", fg="yellow")) |
393 | 446 |
|
| 447 | + # Child component calls |
| 448 | + for child_device_id, child_components in child_device_components.items(): |
| 449 | + for component_id, ver_code in child_components.items(): |
| 450 | + if (requests := get_component_requests(component_id, ver_code)) is not None: |
| 451 | + component_test_calls = [ |
| 452 | + SmartCall( |
| 453 | + module=component_id, |
| 454 | + request=request, |
| 455 | + should_succeed=True, |
| 456 | + child_device_id=child_device_id, |
| 457 | + ) |
| 458 | + for request in requests |
| 459 | + ] |
| 460 | + test_calls.extend(component_test_calls) |
| 461 | + else: |
| 462 | + click.echo(f"Skipping {component_id}..", nl=False) |
| 463 | + click.echo(click.style("UNSUPPORTED", fg="yellow")) |
| 464 | + |
394 | 465 | test_calls.extend(extra_test_calls) |
395 | 466 |
|
| 467 | + return test_calls, successes |
| 468 | + |
| 469 | + |
| 470 | +async def get_smart_fixture(device: SmartDevice, batch_size: int): |
| 471 | + """Get fixture for new TAPO style protocol.""" |
| 472 | + test_calls, successes = await get_smart_test_calls(device) |
| 473 | + |
396 | 474 | for test_call in test_calls: |
397 | 475 | click.echo(f"Testing {test_call.module}..", nl=False) |
398 | 476 | try: |
399 | 477 | click.echo(f"Testing {test_call}..", nl=False) |
400 | | - response = await device.protocol.query( |
401 | | - SmartRequest._create_request_dict(test_call.request) |
402 | | - ) |
| 478 | + if test_call.child_device_id == 0: |
| 479 | + response = await device.protocol.query( |
| 480 | + SmartRequest._create_request_dict(test_call.request) |
| 481 | + ) |
| 482 | + else: |
| 483 | + cp = _ChildProtocolWrapper(test_call.child_device_id, device.protocol) |
| 484 | + response = await cp.query( |
| 485 | + SmartRequest._create_request_dict(test_call.request) |
| 486 | + ) |
403 | 487 | except AuthenticationError as ex: |
404 | 488 | _echo_error( |
405 | 489 | f"Unable to query the device due to an authentication error: {ex}", |
@@ -430,13 +514,48 @@ async def get_smart_fixture(device: SmartDevice, batch_size: int): |
430 | 514 | finally: |
431 | 515 | await device.protocol.close() |
432 | 516 |
|
433 | | - requests = [] |
434 | | - for succ in successes: |
435 | | - requests.append(succ.request) |
| 517 | + device_requests: Dict[str, List[SmartRequest]] = {} |
| 518 | + for success in successes: |
| 519 | + device_request = device_requests.setdefault(success.child_device_id, []) |
| 520 | + device_request.append(success.request) |
| 521 | + |
| 522 | + scrubbed_device_ids = { |
| 523 | + device_id: f"SCRUBBED_CHILD_DEVICE_ID_{index}" |
| 524 | + for index, device_id in enumerate(device_requests.keys()) |
| 525 | + if device_id != "" |
| 526 | + } |
436 | 527 |
|
437 | 528 | final = await _make_requests_or_exit( |
438 | | - device, requests, "all successes at once", batch_size |
| 529 | + device, |
| 530 | + device_requests[""], |
| 531 | + "all successes at once", |
| 532 | + batch_size, |
| 533 | + child_device_id="", |
439 | 534 | ) |
| 535 | + for child_device_id, requests in device_requests.items(): |
| 536 | + if child_device_id == "": |
| 537 | + continue |
| 538 | + response = await _make_requests_or_exit( |
| 539 | + device, |
| 540 | + requests, |
| 541 | + "all child successes at once", |
| 542 | + batch_size, |
| 543 | + child_device_id=child_device_id, |
| 544 | + ) |
| 545 | + scrubbed = scrubbed_device_ids[child_device_id] |
| 546 | + if "get_device_info" in response and "device_id" in response["get_device_info"]: |
| 547 | + response["get_device_info"]["device_id"] = scrubbed |
| 548 | + cd = final.setdefault("child_devices", {}) |
| 549 | + cd[scrubbed] = response |
| 550 | + |
| 551 | + # Scrub the device ids |
| 552 | + if gc := final.get("get_child_device_component_list"): |
| 553 | + for child in gc["child_component_list"]: |
| 554 | + device_id = child["device_id"] |
| 555 | + child["device_id"] = scrubbed_device_ids[device_id] |
| 556 | + for child in final["get_child_device_list"]["child_device_list"]: |
| 557 | + device_id = child["device_id"] |
| 558 | + child["device_id"] = scrubbed_device_ids[device_id] |
440 | 559 |
|
441 | 560 | # Need to recreate a DiscoverResult here because we don't want the aliases |
442 | 561 | # in the fixture, we want the actual field names as returned by the device. |
|