From 029a42e15d7bf6f9dd0ff867d534de5a05787708 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Fri, 13 Dec 2024 18:35:10 +0000 Subject: [PATCH 1/6] Add new methods to dump_devinfo --- devtools/dump_devinfo.py | 37 ++++- devtools/helpers/smartcamrequests.py | 3 + kasa/protocols/smartprotocol.py | 14 +- .../smart/child/S200B(EU)_1.0_1.11.0.json | 10 +- .../fixtures/smartcam/H200(EU)_1.0_1.3.2.json | 148 +++++++++++++++++- 5 files changed, 191 insertions(+), 21 deletions(-) diff --git a/devtools/dump_devinfo.py b/devtools/dump_devinfo.py index 02aebae76..101ed6823 100644 --- a/devtools/dump_devinfo.py +++ b/devtools/dump_devinfo.py @@ -68,6 +68,10 @@ _LOGGER = logging.getLogger(__name__) +SMART_SINGLE_ONLY_CALLS = { + "getConnectStatus", +} + def _wrap_redactors(redactors: dict[str, Callable[[Any], Any] | None]): """Wrap the redactors for dump_devinfo. @@ -233,6 +237,12 @@ async def handle_device( type=bool, help="Set flag if the device encryption uses https.", ) +@click.option( + "--timeout", + required=False, + default=15, + help="Timeout for queries.", +) @click.option("--port", help="Port override", type=int) async def cli( host, @@ -250,6 +260,7 @@ async def cli( device_family, login_version, port, + timeout, ): """Generate devinfo files for devices. @@ -280,6 +291,7 @@ def capture_raw(discovered: DiscoveredRaw): connection_type=connection_type, port_override=port, credentials=credentials, + timeout=timeout, ) device = await Device.connect(config=dc) await handle_device( @@ -301,6 +313,7 @@ def capture_raw(discovered: DiscoveredRaw): port_override=port, credentials=credentials, connection_type=ctype, + timeout=timeout, ) if protocol := get_protocol(config): await handle_device(basedir, autosave, protocol, batch_size=batch_size) @@ -315,11 +328,12 @@ def capture_raw(discovered: DiscoveredRaw): credentials=credentials, port=port, discovery_timeout=discovery_timeout, + timeout=timeout, on_discovered_raw=capture_raw, ) discovery_info = raw_discovery[device.host] if decrypted_data := device._discovery_info.get("decrypted_data"): - discovery_info["decrypted_data"] = decrypted_data + discovery_info["result"]["decrypted_data"] = decrypted_data await handle_device( basedir, autosave, @@ -336,13 +350,14 @@ def capture_raw(discovered: DiscoveredRaw): target=target, credentials=credentials, discovery_timeout=discovery_timeout, + timeout=timeout, on_discovered_raw=capture_raw, ) click.echo(f"Detected {len(devices)} devices") for dev in devices.values(): discovery_info = raw_discovery[dev.host] if decrypted_data := dev._discovery_info.get("decrypted_data"): - discovery_info["decrypted_data"] = decrypted_data + discovery_info["result"]["decrypted_data"] = decrypted_data await handle_device( basedir, @@ -591,7 +606,11 @@ async def get_smart_camera_test_calls(protocol: SmartProtocol): request=request, should_succeed=True, child_device_id="", - supports_multiple=(method != "get"), + supports_multiple=( + method != "get" + and method[:3] == "get" + and method not in SMART_SINGLE_ONLY_CALLS + ), ) ) @@ -925,6 +944,7 @@ async def get_smart_fixtures( and (child_model := response["get_device_info"].get("model")) and child_model != parent_model ): + response = redact_data(response, _wrap_redactors(SMART_REDACTORS)) fixture_results.append(get_smart_child_fixture(response)) else: cd = final.setdefault("child_devices", {}) @@ -940,13 +960,16 @@ async def get_smart_fixtures( child["device_id"] = scrubbed_device_ids[device_id] # Scrub the device ids in the parent for the smart camera protocol - if gc := final.get("getChildDeviceList"): - for child in gc["child_device_list"]: + if gc := final.get("getChildDeviceComponentList"): + for child in gc["child_component_list"]: + device_id = child["device_id"] + child["device_id"] = scrubbed_device_ids[device_id] + for child in final["getChildDeviceList"]["child_device_list"]: if device_id := child.get("device_id"): child["device_id"] = scrubbed_device_ids[device_id] continue - if device_id := child.get("dev_id"): - child["dev_id"] = scrubbed_device_ids[device_id] + elif dev_id := child.get("dev_id"): + child["dev_id"] = scrubbed_device_ids[dev_id] continue _LOGGER.error("Could not find a device for the child device: %s", child) diff --git a/devtools/helpers/smartcamrequests.py b/devtools/helpers/smartcamrequests.py index 074b5774d..5759a44b5 100644 --- a/devtools/helpers/smartcamrequests.py +++ b/devtools/helpers/smartcamrequests.py @@ -60,4 +60,7 @@ {"get": {"motor": {"name": ["capability"]}}}, {"get": {"audio_capability": {"name": ["device_speaker", "device_microphone"]}}}, {"get": {"audio_config": {"name": ["speaker", "microphone"]}}}, + {"getMatterSetupInfo": {"matter": {}}}, + {"getConnectStatus": {"onboarding": {"get_connect_status": {}}}}, + {"scanApList": {"onboarding": {"scan": {}}}}, ] diff --git a/kasa/protocols/smartprotocol.py b/kasa/protocols/smartprotocol.py index 0e092547f..03ad847c3 100644 --- a/kasa/protocols/smartprotocol.py +++ b/kasa/protocols/smartprotocol.py @@ -246,7 +246,19 @@ async def _execute_multiple_query(self, requests: dict, retry_count: int) -> dic responses = response_step["result"]["responses"] for response in responses: - method = response["method"] + # some smartcam devices calls do not populate the method key + # which we can only handle if there's a single request. + if not (method := response.get("method")): + if len(requests) == 1: + method = next(iter(requests)) + else: + _LOGGER.debug( + "No method key in response for %s, skipping: %s", + self._host, + response, + ) + continue + self._handle_response_error_code( response, method, raise_on_error=raise_on_error ) diff --git a/tests/fixtures/smart/child/S200B(EU)_1.0_1.11.0.json b/tests/fixtures/smart/child/S200B(EU)_1.0_1.11.0.json index 9df75fd76..9f49bf201 100644 --- a/tests/fixtures/smart/child/S200B(EU)_1.0_1.11.0.json +++ b/tests/fixtures/smart/child/S200B(EU)_1.0_1.11.0.json @@ -66,10 +66,10 @@ "category": "subg.trigger.button", "device_id": "SCRUBBED_CHILD_DEVICE_ID_1", "fw_ver": "1.11.0 Build 230821 Rel.113553", - "hw_id": "00000000000000000000000000000000", + "hw_id": "9A1EB033BE24834E2A7A6A4E4CF405E8", "hw_ver": "1.0", - "jamming_rssi": -120, - "jamming_signal_level": 1, + "jamming_rssi": -110, + "jamming_signal_level": 2, "lastOnboardingTimestamp": 1714016798, "mac": "202351000000", "model": "S200B", @@ -78,7 +78,7 @@ "parent_device_id": "0000000000000000000000000000000000000000", "region": "Europe/London", "report_interval": 16, - "rssi": -55, + "rssi": -56, "signal_level": 3, "specs": "EU", "status": "online", @@ -99,7 +99,7 @@ }, "get_latest_fw": { "fw_ver": "1.12.0 Build 231121 Rel.092444", - "hw_id": "00000000000000000000000000000000", + "hw_id": "9A1EB033BE24834E2A7A6A4E4CF405E8", "need_to_upgrade": true, "oem_id": "00000000000000000000000000000000", "release_date": "2024-04-02", diff --git a/tests/fixtures/smartcam/H200(EU)_1.0_1.3.2.json b/tests/fixtures/smartcam/H200(EU)_1.0_1.3.2.json index 9ccaa7e0e..fd8511b37 100644 --- a/tests/fixtures/smartcam/H200(EU)_1.0_1.3.2.json +++ b/tests/fixtures/smartcam/H200(EU)_1.0_1.3.2.json @@ -26,6 +26,7 @@ "firmware_version": "1.3.2 Build 20240424 rel.75425", "hardware_version": "1.0", "ip": "127.0.0.123", + "isResetWiFi": false, "is_support_iot_cloud": true, "mac": "A8-6E-84-00-00-00", "mgt_encrypt_schm": { @@ -212,9 +213,9 @@ "category": "subg.trigger.button", "device_id": "SCRUBBED_CHILD_DEVICE_ID_1", "fw_ver": "1.11.0 Build 230821 Rel.113553", - "hw_id": "00000000000000000000000000000000", + "hw_id": "9A1EB033BE24834E2A7A6A4E4CF405E8", "hw_ver": "1.0", - "jamming_rssi": -108, + "jamming_rssi": -110, "jamming_signal_level": 2, "lastOnboardingTimestamp": 1714016798, "mac": "202351000000", @@ -224,7 +225,7 @@ "parent_device_id": "0000000000000000000000000000000000000000", "region": "Europe/London", "report_interval": 16, - "rssi": -66, + "rssi": -56, "signal_level": 3, "specs": "EU", "status": "online", @@ -245,8 +246,19 @@ "getClockStatus": { "system": { "clock_status": { - "local_time": "2024-11-01 13:56:27", - "seconds_from_1970": 1730469387 + "local_time": "2024-12-13 18:34:09", + "seconds_from_1970": 1734114849 + } + } + }, + "getConnectStatus": { + "getConnectStatus": { + "onboarding": { + "get_connect_status": { + "current_ssid": "", + "err_code": 0, + "status": 0 + } } } }, @@ -266,7 +278,7 @@ "device_name": "#MASKED_NAME#", "device_type": "SMART.TAPOHUB", "has_set_location_info": 1, - "hw_id": "00000000000000000000000000000000", + "hw_id": "99A36151C79A3A5D72823C98BC0FDA5F", "hw_version": "1.0", "latitude": 0, "longitude": 0, @@ -289,7 +301,7 @@ "device_name": "#MASKED_NAME#", "device_type": "SMART.TAPOHUB", "has_set_location_info": 1, - "hw_id": "00000000000000000000000000000000", + "hw_id": "99A36151C79A3A5D72823C98BC0FDA5F", "hw_version": "1.0", "latitude": 0, "longitude": 0, @@ -329,6 +341,10 @@ } } }, + "getMatterSetupInfo": { + "setup_code": "00000000000", + "setup_payload": "00:000000-000000000000" + }, "getMediaEncrypt": { "cet": { "media_encrypt": { @@ -353,7 +369,7 @@ "getSirenConfig": { "duration": 300, "siren_type": "Doorbell Ring 1", - "volume": "6" + "volume": "1" }, "getSirenStatus": { "status": "off", @@ -389,5 +405,121 @@ "zone_id": "Europe/London" } } + }, + "scanApList": { + "scanApList": { + "onboarding": { + "scan": { + "ap_list": [ + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 2, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 1, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 1, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 0, + "bssid": "000000000000", + "encryption": 0, + "rssi": 1, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 1, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 0, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 3, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 3, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 4, + "bssid": "000000000000", + "encryption": 2, + "rssi": 0, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 0, + "bssid": "000000000000", + "encryption": 0, + "rssi": 0, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 0, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 1, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 4, + "bssid": "000000000000", + "encryption": 3, + "rssi": 2, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 1, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 1, + "ssid": "I01BU0tFRF9TU0lEIw==" + } + ], + "wpa3_supported": "false" + } + } + } } } From 761a29e59de9d8ef60854a66d7bba04b5f5e00e8 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Sun, 15 Dec 2024 11:14:38 +0000 Subject: [PATCH 2/6] Single only requests --- devtools/dump_devinfo.py | 10 +- kasa/protocols/smartprotocol.py | 28 +- .../smart/child/S200B(EU)_1.0_1.11.0.json | 6 +- .../fixtures/smartcam/C210(EU)_2.0_1.4.3.json | 65 ++++- .../fixtures/smartcam/H200(EU)_1.0_1.3.2.json | 244 +++++++++--------- 5 files changed, 199 insertions(+), 154 deletions(-) diff --git a/devtools/dump_devinfo.py b/devtools/dump_devinfo.py index 101ed6823..698515750 100644 --- a/devtools/dump_devinfo.py +++ b/devtools/dump_devinfo.py @@ -68,10 +68,6 @@ _LOGGER = logging.getLogger(__name__) -SMART_SINGLE_ONLY_CALLS = { - "getConnectStatus", -} - def _wrap_redactors(redactors: dict[str, Callable[[Any], Any] | None]): """Wrap the redactors for dump_devinfo. @@ -606,11 +602,7 @@ async def get_smart_camera_test_calls(protocol: SmartProtocol): request=request, should_succeed=True, child_device_id="", - supports_multiple=( - method != "get" - and method[:3] == "get" - and method not in SMART_SINGLE_ONLY_CALLS - ), + supports_multiple=(method != "get"), ) ) diff --git a/kasa/protocols/smartprotocol.py b/kasa/protocols/smartprotocol.py index 03ad847c3..72265b031 100644 --- a/kasa/protocols/smartprotocol.py +++ b/kasa/protocols/smartprotocol.py @@ -69,6 +69,13 @@ "map_data": lambda x: "#SCRUBBED_MAPDATA#" if x else "", } +# Queries that are known not to work properly when sent as a +# multiRequest. They will not return the `method` key. +DO_NOT_SEND_AS_MULTI_REQUEST = { + "getConnectStatus", + "scanApList", +} + class SmartProtocol(BaseProtocol): """Class for the new TPLink SMART protocol.""" @@ -89,6 +96,7 @@ def __init__( self._transport._config.batch_size or self.DEFAULT_MULTI_REQUEST_BATCH_SIZE ) self._redact_data = True + self._method_missing_logged = False def get_smart_request(self, method: str, params: dict | None = None) -> str: """Get a request message as a string.""" @@ -178,6 +186,7 @@ async def _execute_multiple_query(self, requests: dict, retry_count: int) -> dic multi_requests = [ {"method": method, "params": params} if params else {"method": method} for method, params in requests.items() + if method not in DO_NOT_SEND_AS_MULTI_REQUEST ] end = len(multi_requests) @@ -247,17 +256,18 @@ async def _execute_multiple_query(self, requests: dict, retry_count: int) -> dic responses = response_step["result"]["responses"] for response in responses: # some smartcam devices calls do not populate the method key - # which we can only handle if there's a single request. + # these should be defined in DO_NOT_SEND_AS_MULTI_REQUEST. if not (method := response.get("method")): - if len(requests) == 1: - method = next(iter(requests)) - else: - _LOGGER.debug( + if not self._method_missing_logged: + # Avoid spamming the logs + self._method_missing_logged = True + _LOGGER.error( "No method key in response for %s, skipping: %s", self._host, - response, + response_step, ) - continue + # These will end up being queried individually + continue self._handle_response_error_code( response, method, raise_on_error=raise_on_error @@ -267,7 +277,9 @@ async def _execute_multiple_query(self, requests: dict, retry_count: int) -> dic result, method, retry_count=retry_count ) multi_result[method] = result - # Multi requests don't continue after errors so requery any missing + + # Multi requests don't continue after errors so requery any missing. + # Will also query individually any DO_NOT_SEND_AS_MULTI_REQUEST. for method, params in requests.items(): if method not in multi_result: resp = await self._transport.send( diff --git a/tests/fixtures/smart/child/S200B(EU)_1.0_1.11.0.json b/tests/fixtures/smart/child/S200B(EU)_1.0_1.11.0.json index 9f49bf201..2c45c0c31 100644 --- a/tests/fixtures/smart/child/S200B(EU)_1.0_1.11.0.json +++ b/tests/fixtures/smart/child/S200B(EU)_1.0_1.11.0.json @@ -68,8 +68,8 @@ "fw_ver": "1.11.0 Build 230821 Rel.113553", "hw_id": "9A1EB033BE24834E2A7A6A4E4CF405E8", "hw_ver": "1.0", - "jamming_rssi": -110, - "jamming_signal_level": 2, + "jamming_rssi": -119, + "jamming_signal_level": 1, "lastOnboardingTimestamp": 1714016798, "mac": "202351000000", "model": "S200B", @@ -78,7 +78,7 @@ "parent_device_id": "0000000000000000000000000000000000000000", "region": "Europe/London", "report_interval": 16, - "rssi": -56, + "rssi": -60, "signal_level": 3, "specs": "EU", "status": "online", diff --git a/tests/fixtures/smartcam/C210(EU)_2.0_1.4.3.json b/tests/fixtures/smartcam/C210(EU)_2.0_1.4.3.json index b62801183..c3d0fb0f1 100644 --- a/tests/fixtures/smartcam/C210(EU)_2.0_1.4.3.json +++ b/tests/fixtures/smartcam/C210(EU)_2.0_1.4.3.json @@ -7,8 +7,8 @@ "connect_type": "wireless", "device_id": "0000000000000000000000000000000000000000", "http_port": 443, - "last_alarm_time": "0", - "last_alarm_type": "", + "last_alarm_time": "1733422805", + "last_alarm_type": "motion", "owner": "00000000000000000000000000000000", "sd_status": "offline" }, @@ -32,7 +32,8 @@ "mac": "40-AE-30-00-00-00", "mgt_encrypt_schm": { "is_support_https": true - } + }, + "protocol_version": 1 } }, "getAlertConfig": { @@ -266,15 +267,22 @@ "getClockStatus": { "system": { "clock_status": { - "local_time": "2024-11-01 13:58:50", - "seconds_from_1970": 1730469530 + "local_time": "2024-12-15 11:09:14", + "seconds_from_1970": 1734260954 + } + } + }, + "getConnectStatus": { + "onboarding": { + "get_connect_status": { + "status": 0 } } }, "getConnectionType": { "link_type": "wifi", "rssi": "3", - "rssiValue": -57, + "rssiValue": -60, "ssid": "I01BU0tFRF9TU0lEIw==" }, "getDetectionConfig": { @@ -304,7 +312,7 @@ "ffs": false, "has_set_location_info": 1, "hw_desc": "00000000000000000000000000000000", - "hw_id": "00000000000000000000000000000000", + "hw_id": "5FAAA6EC6A1A74A8B50B0C3302FB26FD", "hw_version": "2.0", "is_cal": true, "latitude": 0, @@ -321,7 +329,7 @@ "getFirmwareAutoUpgradeConfig": { "auto_upgrade": { "common": { - "enabled": "on", + "enabled": "off", "random_range": "120", "time": "03:00" } @@ -338,8 +346,8 @@ "getLastAlarmInfo": { "system": { "last_alarm_info": { - "last_alarm_time": "0", - "last_alarm_type": "" + "last_alarm_time": "1733422805", + "last_alarm_type": "motion" } } }, @@ -961,5 +969,42 @@ } } } + }, + "scanApList": { + "onboarding": { + "scan": { + "ap_list": [ + { + "auth": 4, + "bssid": "000000000000", + "encryption": 3, + "rssi": 2, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 4, + "bssid": "000000000000", + "encryption": 3, + "rssi": 1, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 0, + "bssid": "000000000000", + "encryption": 0, + "rssi": 1, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 4, + "bssid": "000000000000", + "encryption": 3, + "rssi": 1, + "ssid": "I01BU0tFRF9TU0lEIw==" + } + ], + "wpa3_supported": "false" + } + } } } diff --git a/tests/fixtures/smartcam/H200(EU)_1.0_1.3.2.json b/tests/fixtures/smartcam/H200(EU)_1.0_1.3.2.json index fd8511b37..4f0d11350 100644 --- a/tests/fixtures/smartcam/H200(EU)_1.0_1.3.2.json +++ b/tests/fixtures/smartcam/H200(EU)_1.0_1.3.2.json @@ -215,8 +215,8 @@ "fw_ver": "1.11.0 Build 230821 Rel.113553", "hw_id": "9A1EB033BE24834E2A7A6A4E4CF405E8", "hw_ver": "1.0", - "jamming_rssi": -110, - "jamming_signal_level": 2, + "jamming_rssi": -119, + "jamming_signal_level": 1, "lastOnboardingTimestamp": 1714016798, "mac": "202351000000", "model": "S200B", @@ -225,7 +225,7 @@ "parent_device_id": "0000000000000000000000000000000000000000", "region": "Europe/London", "report_interval": 16, - "rssi": -56, + "rssi": -60, "signal_level": 3, "specs": "EU", "status": "online", @@ -246,19 +246,17 @@ "getClockStatus": { "system": { "clock_status": { - "local_time": "2024-12-13 18:34:09", - "seconds_from_1970": 1734114849 + "local_time": "1984-10-21 23:07:50", + "seconds_from_1970": 467244470 } } }, "getConnectStatus": { - "getConnectStatus": { - "onboarding": { - "get_connect_status": { - "current_ssid": "", - "err_code": 0, - "status": 0 - } + "onboarding": { + "get_connect_status": { + "current_ssid": "", + "err_code": 0, + "status": 0 } } }, @@ -407,118 +405,116 @@ } }, "scanApList": { - "scanApList": { - "onboarding": { - "scan": { - "ap_list": [ - { - "auth": 3, - "bssid": "000000000000", - "encryption": 2, - "rssi": 2, - "ssid": "I01BU0tFRF9TU0lEIw==" - }, - { - "auth": 3, - "bssid": "000000000000", - "encryption": 2, - "rssi": 1, - "ssid": "I01BU0tFRF9TU0lEIw==" - }, - { - "auth": 3, - "bssid": "000000000000", - "encryption": 2, - "rssi": 1, - "ssid": "I01BU0tFRF9TU0lEIw==" - }, - { - "auth": 0, - "bssid": "000000000000", - "encryption": 0, - "rssi": 1, - "ssid": "I01BU0tFRF9TU0lEIw==" - }, - { - "auth": 3, - "bssid": "000000000000", - "encryption": 2, - "rssi": 1, - "ssid": "I01BU0tFRF9TU0lEIw==" - }, - { - "auth": 3, - "bssid": "000000000000", - "encryption": 2, - "rssi": 0, - "ssid": "I01BU0tFRF9TU0lEIw==" - }, - { - "auth": 3, - "bssid": "000000000000", - "encryption": 2, - "rssi": 3, - "ssid": "I01BU0tFRF9TU0lEIw==" - }, - { - "auth": 3, - "bssid": "000000000000", - "encryption": 2, - "rssi": 3, - "ssid": "I01BU0tFRF9TU0lEIw==" - }, - { - "auth": 4, - "bssid": "000000000000", - "encryption": 2, - "rssi": 0, - "ssid": "I01BU0tFRF9TU0lEIw==" - }, - { - "auth": 0, - "bssid": "000000000000", - "encryption": 0, - "rssi": 0, - "ssid": "I01BU0tFRF9TU0lEIw==" - }, - { - "auth": 3, - "bssid": "000000000000", - "encryption": 2, - "rssi": 0, - "ssid": "I01BU0tFRF9TU0lEIw==" - }, - { - "auth": 3, - "bssid": "000000000000", - "encryption": 2, - "rssi": 1, - "ssid": "I01BU0tFRF9TU0lEIw==" - }, - { - "auth": 4, - "bssid": "000000000000", - "encryption": 3, - "rssi": 2, - "ssid": "I01BU0tFRF9TU0lEIw==" - }, - { - "auth": 3, - "bssid": "000000000000", - "encryption": 2, - "rssi": 1, - "ssid": "I01BU0tFRF9TU0lEIw==" - }, - { - "auth": 3, - "bssid": "000000000000", - "encryption": 2, - "rssi": 1, - "ssid": "I01BU0tFRF9TU0lEIw==" - } - ], - "wpa3_supported": "false" - } + "onboarding": { + "scan": { + "ap_list": [ + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 2, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 0, + "bssid": "000000000000", + "encryption": 0, + "rssi": 1, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 1, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 1, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 1, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 1, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 4, + "bssid": "000000000000", + "encryption": 3, + "rssi": 0, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 0, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 3, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 3, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 0, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 2, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 4, + "bssid": "000000000000", + "encryption": 3, + "rssi": 2, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 1, + "ssid": "I01BU0tFRF9TU0lEIw==" + }, + { + "auth": 3, + "bssid": "000000000000", + "encryption": 2, + "rssi": 1, + "ssid": "I01BU0tFRF9TU0lEIw==" + } + ], + "wpa3_supported": "false" } } } From bb9ba545c0975a06df775f0b19b278be3da6725d Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Sun, 15 Dec 2024 11:22:06 +0000 Subject: [PATCH 3/6] Add hw_ver and fw_ver to redact --- kasa/protocols/smartprotocol.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kasa/protocols/smartprotocol.py b/kasa/protocols/smartprotocol.py index 72265b031..5e7935694 100644 --- a/kasa/protocols/smartprotocol.py +++ b/kasa/protocols/smartprotocol.py @@ -50,6 +50,8 @@ "bssid": lambda _: "000000000000", "channel": lambda _: 0, "oem_id": lambda x: "REDACTED_" + x[9::], + "hw_id": lambda x: "REDACTED_" + x[9::], + "fw_id": lambda x: "REDACTED_" + x[9::], "setup_code": lambda x: re.sub(r"\w", "0", x), # matter "setup_payload": lambda x: re.sub(r"\w", "0", x), # matter "mfi_setup_code": lambda x: re.sub(r"\w", "0", x), # mfi_ for homekit From 256e1099955991bbc26a2bd27d8a26c57f70297f Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Sun, 15 Dec 2024 11:34:05 +0000 Subject: [PATCH 4/6] Clean up fixtures --- .../smart/child/S200B(EU)_1.0_1.11.0.json | 8 ++-- .../fixtures/smartcam/C210(EU)_2.0_1.4.3.json | 17 +++----- .../fixtures/smartcam/H200(EU)_1.0_1.3.2.json | 41 +++++-------------- 3 files changed, 19 insertions(+), 47 deletions(-) diff --git a/tests/fixtures/smart/child/S200B(EU)_1.0_1.11.0.json b/tests/fixtures/smart/child/S200B(EU)_1.0_1.11.0.json index 2c45c0c31..9df75fd76 100644 --- a/tests/fixtures/smart/child/S200B(EU)_1.0_1.11.0.json +++ b/tests/fixtures/smart/child/S200B(EU)_1.0_1.11.0.json @@ -66,9 +66,9 @@ "category": "subg.trigger.button", "device_id": "SCRUBBED_CHILD_DEVICE_ID_1", "fw_ver": "1.11.0 Build 230821 Rel.113553", - "hw_id": "9A1EB033BE24834E2A7A6A4E4CF405E8", + "hw_id": "00000000000000000000000000000000", "hw_ver": "1.0", - "jamming_rssi": -119, + "jamming_rssi": -120, "jamming_signal_level": 1, "lastOnboardingTimestamp": 1714016798, "mac": "202351000000", @@ -78,7 +78,7 @@ "parent_device_id": "0000000000000000000000000000000000000000", "region": "Europe/London", "report_interval": 16, - "rssi": -60, + "rssi": -55, "signal_level": 3, "specs": "EU", "status": "online", @@ -99,7 +99,7 @@ }, "get_latest_fw": { "fw_ver": "1.12.0 Build 231121 Rel.092444", - "hw_id": "9A1EB033BE24834E2A7A6A4E4CF405E8", + "hw_id": "00000000000000000000000000000000", "need_to_upgrade": true, "oem_id": "00000000000000000000000000000000", "release_date": "2024-04-02", diff --git a/tests/fixtures/smartcam/C210(EU)_2.0_1.4.3.json b/tests/fixtures/smartcam/C210(EU)_2.0_1.4.3.json index c3d0fb0f1..d4de5b9f2 100644 --- a/tests/fixtures/smartcam/C210(EU)_2.0_1.4.3.json +++ b/tests/fixtures/smartcam/C210(EU)_2.0_1.4.3.json @@ -267,8 +267,8 @@ "getClockStatus": { "system": { "clock_status": { - "local_time": "2024-12-15 11:09:14", - "seconds_from_1970": 1734260954 + "local_time": "2024-12-15 11:28:40", + "seconds_from_1970": 1734262120 } } }, @@ -282,7 +282,7 @@ "getConnectionType": { "link_type": "wifi", "rssi": "3", - "rssiValue": -60, + "rssiValue": -61, "ssid": "I01BU0tFRF9TU0lEIw==" }, "getDetectionConfig": { @@ -312,7 +312,7 @@ "ffs": false, "has_set_location_info": 1, "hw_desc": "00000000000000000000000000000000", - "hw_id": "5FAAA6EC6A1A74A8B50B0C3302FB26FD", + "hw_id": "00000000000000000000000000000000", "hw_version": "2.0", "is_cal": true, "latitude": 0, @@ -988,18 +988,11 @@ "rssi": 1, "ssid": "I01BU0tFRF9TU0lEIw==" }, - { - "auth": 0, - "bssid": "000000000000", - "encryption": 0, - "rssi": 1, - "ssid": "I01BU0tFRF9TU0lEIw==" - }, { "auth": 4, "bssid": "000000000000", "encryption": 3, - "rssi": 1, + "rssi": 0, "ssid": "I01BU0tFRF9TU0lEIw==" } ], diff --git a/tests/fixtures/smartcam/H200(EU)_1.0_1.3.2.json b/tests/fixtures/smartcam/H200(EU)_1.0_1.3.2.json index 4f0d11350..4ef99fae2 100644 --- a/tests/fixtures/smartcam/H200(EU)_1.0_1.3.2.json +++ b/tests/fixtures/smartcam/H200(EU)_1.0_1.3.2.json @@ -213,7 +213,7 @@ "category": "subg.trigger.button", "device_id": "SCRUBBED_CHILD_DEVICE_ID_1", "fw_ver": "1.11.0 Build 230821 Rel.113553", - "hw_id": "9A1EB033BE24834E2A7A6A4E4CF405E8", + "hw_id": "00000000000000000000000000000000", "hw_ver": "1.0", "jamming_rssi": -119, "jamming_signal_level": 1, @@ -246,8 +246,8 @@ "getClockStatus": { "system": { "clock_status": { - "local_time": "1984-10-21 23:07:50", - "seconds_from_1970": 467244470 + "local_time": "1984-10-21 23:48:23", + "seconds_from_1970": 467246903 } } }, @@ -276,7 +276,7 @@ "device_name": "#MASKED_NAME#", "device_type": "SMART.TAPOHUB", "has_set_location_info": 1, - "hw_id": "99A36151C79A3A5D72823C98BC0FDA5F", + "hw_id": "00000000000000000000000000000000", "hw_version": "1.0", "latitude": 0, "longitude": 0, @@ -299,7 +299,7 @@ "device_name": "#MASKED_NAME#", "device_type": "SMART.TAPOHUB", "has_set_location_info": 1, - "hw_id": "99A36151C79A3A5D72823C98BC0FDA5F", + "hw_id": "00000000000000000000000000000000", "hw_version": "1.0", "latitude": 0, "longitude": 0, @@ -415,13 +415,6 @@ "rssi": 2, "ssid": "I01BU0tFRF9TU0lEIw==" }, - { - "auth": 0, - "bssid": "000000000000", - "encryption": 0, - "rssi": 1, - "ssid": "I01BU0tFRF9TU0lEIw==" - }, { "auth": 3, "bssid": "000000000000", @@ -430,10 +423,10 @@ "ssid": "I01BU0tFRF9TU0lEIw==" }, { - "auth": 3, + "auth": 0, "bssid": "000000000000", - "encryption": 2, - "rssi": 1, + "encryption": 0, + "rssi": 2, "ssid": "I01BU0tFRF9TU0lEIw==" }, { @@ -450,18 +443,11 @@ "rssi": 1, "ssid": "I01BU0tFRF9TU0lEIw==" }, - { - "auth": 4, - "bssid": "000000000000", - "encryption": 3, - "rssi": 0, - "ssid": "I01BU0tFRF9TU0lEIw==" - }, { "auth": 3, "bssid": "000000000000", "encryption": 2, - "rssi": 0, + "rssi": 1, "ssid": "I01BU0tFRF9TU0lEIw==" }, { @@ -482,14 +468,7 @@ "auth": 3, "bssid": "000000000000", "encryption": 2, - "rssi": 0, - "ssid": "I01BU0tFRF9TU0lEIw==" - }, - { - "auth": 3, - "bssid": "000000000000", - "encryption": 2, - "rssi": 2, + "rssi": 1, "ssid": "I01BU0tFRF9TU0lEIw==" }, { From 1a0c6da4c8b00f97fe8ad5088f32600d55a5c95b Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Mon, 16 Dec 2024 08:52:51 +0000 Subject: [PATCH 5/6] Ensure errors are raised for single only requests --- kasa/protocols/smartprotocol.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/kasa/protocols/smartprotocol.py b/kasa/protocols/smartprotocol.py index e187785d1..7f02b45e7 100644 --- a/kasa/protocols/smartprotocol.py +++ b/kasa/protocols/smartprotocol.py @@ -185,18 +185,18 @@ async def _execute_multiple_query(self, requests: dict, retry_count: int) -> dic multi_result: dict[str, Any] = {} smart_method = "multipleRequest" + end = len(requests) + # The SmartCamProtocol sends requests with a length 1 as a + # multipleRequest. The SmartProtocol doesn't so will never + # raise_on_error + raise_on_error = end == 1 + multi_requests = [ {"method": method, "params": params} if params else {"method": method} for method, params in requests.items() if method not in FORCE_SINGLE_REQUEST ] - end = len(multi_requests) - # The SmartCamProtocol sends requests with a length 1 as a - # multipleRequest. The SmartProtocol doesn't so will never - # raise_on_error - raise_on_error = end == 1 - # Break the requests down as there can be a size limit step = self._multi_request_batch_size if step == 1: @@ -287,7 +287,9 @@ async def _execute_multiple_query(self, requests: dict, retry_count: int) -> dic resp = await self._transport.send( self.get_smart_request(method, params) ) - self._handle_response_error_code(resp, method, raise_on_error=False) + self._handle_response_error_code( + resp, method, raise_on_error=raise_on_error + ) multi_result[method] = resp.get("result") return multi_result From 99d5cc0fab91e24761d8ede5287ed201dddc926a Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Mon, 16 Dec 2024 09:08:00 +0000 Subject: [PATCH 6/6] Fix test smartcam framework --- tests/fakeprotocol_smartcam.py | 55 ++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/tests/fakeprotocol_smartcam.py b/tests/fakeprotocol_smartcam.py index e8cc6f301..381a0a89c 100644 --- a/tests/fakeprotocol_smartcam.py +++ b/tests/fakeprotocol_smartcam.py @@ -221,35 +221,38 @@ async def _send_request(self, request_dict: dict): return {**result, "error_code": 0} else: return {"error_code": -1} - elif method[:3] == "get": + + if method in info: params = request_dict.get("params") - if method in info: - result = copy.deepcopy(info[method]) - if "start_index" in result and "sum" in result: - list_key = next( - iter([key for key in result if isinstance(result[key], list)]) - ) - start_index = ( - start_index - if (params and (start_index := params.get("start_index"))) - else 0 - ) - - result[list_key] = result[list_key][ - start_index : start_index + self.list_return_size - ] - return {"result": result, "error_code": 0} - if ( - # FIXTURE_MISSING is for service calls not in place when - # SMART fixtures started to be generated - missing_result := self.FIXTURE_MISSING_MAP.get(method) - ) and missing_result[0] in self.components: - # Copy to info so it will work with update methods - info[method] = copy.deepcopy(missing_result[1]) - result = copy.deepcopy(info[method]) - return {"result": result, "error_code": 0} + result = copy.deepcopy(info[method]) + if "start_index" in result and "sum" in result: + list_key = next( + iter([key for key in result if isinstance(result[key], list)]) + ) + start_index = ( + start_index + if (params and (start_index := params.get("start_index"))) + else 0 + ) + + result[list_key] = result[list_key][ + start_index : start_index + self.list_return_size + ] + return {"result": result, "error_code": 0} + if self.verbatim: return {"error_code": -1} + + if ( + # FIXTURE_MISSING is for service calls not in place when + # SMART fixtures started to be generated + missing_result := self.FIXTURE_MISSING_MAP.get(method) + ) and missing_result[0] in self.components: + # Copy to info so it will work with update methods + info[method] = copy.deepcopy(missing_result[1]) + result = copy.deepcopy(info[method]) + return {"result": result, "error_code": 0} + return {"error_code": -1} async def close(self) -> None: