mirror of
				https://github.com/python-kasa/python-kasa.git
				synced 2025-10-31 20:51:54 +00:00 
			
		
		
		
	Allow https for klaptransport (#1415)
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				CI / Perform linting checks (3.13) (push) Waiting to run
				
			
		
			
				
	
				CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, macos-latest, 3.11) (push) Blocked by required conditions
				
			
		
			
				
	
				CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, macos-latest, 3.12) (push) Blocked by required conditions
				
			
		
			
				
	
				CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, macos-latest, 3.13) (push) Blocked by required conditions
				
			
		
			
				
	
				CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, ubuntu-latest, 3.11) (push) Blocked by required conditions
				
			
		
			
				
	
				CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, ubuntu-latest, 3.12) (push) Blocked by required conditions
				
			
		
			
				
	
				CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, ubuntu-latest, 3.13) (push) Blocked by required conditions
				
			
		
			
				
	
				CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, windows-latest, 3.11) (push) Blocked by required conditions
				
			
		
			
				
	
				CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, windows-latest, 3.12) (push) Blocked by required conditions
				
			
		
			
				
	
				CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, windows-latest, 3.13) (push) Blocked by required conditions
				
			
		
			
				
	
				CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (true, ubuntu-latest, 3.11) (push) Blocked by required conditions
				
			
		
			
				
	
				CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (true, ubuntu-latest, 3.12) (push) Blocked by required conditions
				
			
		
			
				
	
				CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (true, ubuntu-latest, 3.13) (push) Blocked by required conditions
				
			
		
			
				
	
				CodeQL checks / Analyze (python) (push) Has been cancelled
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	CI / Perform linting checks (3.13) (push) Waiting to run
				
			CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, macos-latest, 3.11) (push) Blocked by required conditions
				
			CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, macos-latest, 3.12) (push) Blocked by required conditions
				
			CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, macos-latest, 3.13) (push) Blocked by required conditions
				
			CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, ubuntu-latest, 3.11) (push) Blocked by required conditions
				
			CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, ubuntu-latest, 3.12) (push) Blocked by required conditions
				
			CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, ubuntu-latest, 3.13) (push) Blocked by required conditions
				
			CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, windows-latest, 3.11) (push) Blocked by required conditions
				
			CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, windows-latest, 3.12) (push) Blocked by required conditions
				
			CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (false, windows-latest, 3.13) (push) Blocked by required conditions
				
			CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (true, ubuntu-latest, 3.11) (push) Blocked by required conditions
				
			CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (true, ubuntu-latest, 3.12) (push) Blocked by required conditions
				
			CI / Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (extras)", ""]')[matrix.extras == ''] }} (true, ubuntu-latest, 3.13) (push) Blocked by required conditions
				
			CodeQL checks / Analyze (python) (push) Has been cancelled
				
			Later firmware versions on robovacs use `KLAP` over https instead of ssltransport (reported as AES)
This commit is contained in:
		| @@ -204,7 +204,7 @@ The following devices have been tested and confirmed as working. If your device | ||||
| - **Cameras**: C100, C210, C220, C225, C325WB, C520WS, C720, D230, TC65, TC70 | ||||
| - **Hubs**: H100, H200 | ||||
| - **Hub-Connected Devices[^3]**: S200B, S200D, T100, T110, T300, T310, T315 | ||||
| - **Vacuums**: RV20 Max Plus | ||||
| - **Vacuums**: RV20 Max Plus, RV30 Max | ||||
|  | ||||
| <!--SUPPORTED_END--> | ||||
| [^1]: Model requires authentication | ||||
|   | ||||
| @@ -330,6 +330,8 @@ All Tapo devices require authentication.<br>Hub-Connected Devices may work acros | ||||
|  | ||||
| - **RV20 Max Plus** | ||||
|   - Hardware: 1.0 (EU) / Firmware: 1.0.7 | ||||
| - **RV30 Max** | ||||
|   - Hardware: 1.0 (US) / Firmware: 1.2.0 | ||||
|  | ||||
|  | ||||
| <!--SUPPORTED_END--> | ||||
|   | ||||
| @@ -300,7 +300,9 @@ async def cli( | ||||
|             connection_type = DeviceConnectionParameters.from_values( | ||||
|                 dr.device_type, | ||||
|                 dr.mgt_encrypt_schm.encrypt_type, | ||||
|                 dr.mgt_encrypt_schm.lv, | ||||
|                 login_version=dr.mgt_encrypt_schm.lv, | ||||
|                 https=dr.mgt_encrypt_schm.is_support_https, | ||||
|                 http_port=dr.mgt_encrypt_schm.http_port, | ||||
|             ) | ||||
|             dc = DeviceConfig( | ||||
|                 host=host, | ||||
|   | ||||
| @@ -261,8 +261,11 @@ async def config(ctx: click.Context) -> DeviceDict: | ||||
|     host_port = host + (f":{port}" if port else "") | ||||
|  | ||||
|     def on_attempt(connect_attempt: ConnectAttempt, success: bool) -> None: | ||||
|         prot, tran, dev = connect_attempt | ||||
|         key_str = f"{prot.__name__} + {tran.__name__} + {dev.__name__}" | ||||
|         prot, tran, dev, https = connect_attempt | ||||
|         key_str = ( | ||||
|             f"{prot.__name__} + {tran.__name__} + {dev.__name__}" | ||||
|             f" + {'https' if https else 'http'}" | ||||
|         ) | ||||
|         result = "succeeded" if success else "failed" | ||||
|         msg = f"Attempt to connect to {host_port} with {key_str} {result}" | ||||
|         echo(msg) | ||||
|   | ||||
| @@ -189,6 +189,7 @@ def get_protocol(config: DeviceConfig, *, strict: bool = False) -> BaseProtocol | ||||
|     :param config: Device config to derive protocol | ||||
|     :param strict: Require exact match on encrypt type | ||||
|     """ | ||||
|     _LOGGER.debug("Finding protocol for %s", config.host) | ||||
|     ctype = config.connection_type | ||||
|     protocol_name = ctype.device_family.value.split(".")[0] | ||||
|     _LOGGER.debug("Finding protocol for %s", ctype.device_family) | ||||
| @@ -203,9 +204,11 @@ def get_protocol(config: DeviceConfig, *, strict: bool = False) -> BaseProtocol | ||||
|             return None | ||||
|         return IotProtocol(transport=LinkieTransportV2(config=config)) | ||||
|  | ||||
|     if ctype.device_family is DeviceFamily.SmartTapoRobovac: | ||||
|         if strict and ctype.encryption_type is not DeviceEncryptionType.Aes: | ||||
|             return None | ||||
|     # Older FW used a different transport | ||||
|     if ( | ||||
|         ctype.device_family is DeviceFamily.SmartTapoRobovac | ||||
|         and ctype.encryption_type is DeviceEncryptionType.Aes | ||||
|     ): | ||||
|         return SmartProtocol(transport=SslTransport(config=config)) | ||||
|  | ||||
|     protocol_transport_key = ( | ||||
| @@ -223,6 +226,7 @@ def get_protocol(config: DeviceConfig, *, strict: bool = False) -> BaseProtocol | ||||
|         "IOT.KLAP": (IotProtocol, KlapTransport), | ||||
|         "SMART.AES": (SmartProtocol, AesTransport), | ||||
|         "SMART.KLAP": (SmartProtocol, KlapTransportV2), | ||||
|         "SMART.KLAP.HTTPS": (SmartProtocol, KlapTransportV2), | ||||
|         # H200 is device family SMART.TAPOHUB and uses SmartCamProtocol so use | ||||
|         # https to distuingish from SmartProtocol devices | ||||
|         "SMART.AES.HTTPS": (SmartCamProtocol, SslAesTransport), | ||||
|   | ||||
| @@ -20,7 +20,7 @@ None | ||||
| {'host': '127.0.0.3', 'timeout': 5, 'credentials': {'username': 'user@example.com', \ | ||||
| 'password': 'great_password'}, 'connection_type'\ | ||||
| : {'device_family': 'SMART.TAPOBULB', 'encryption_type': 'KLAP', 'login_version': 2, \ | ||||
| 'https': False}} | ||||
| 'https': False, 'http_port': 80}} | ||||
|  | ||||
| >>> later_device = await Device.connect(config=Device.Config.from_dict(config_dict)) | ||||
| >>> print(later_device.alias)  # Alias is available as connect() calls update() | ||||
| @@ -98,13 +98,16 @@ class DeviceConnectionParameters(_DeviceConfigBaseMixin): | ||||
|     encryption_type: DeviceEncryptionType | ||||
|     login_version: int | None = None | ||||
|     https: bool = False | ||||
|     http_port: int | None = None | ||||
|  | ||||
|     @staticmethod | ||||
|     def from_values( | ||||
|         device_family: str, | ||||
|         encryption_type: str, | ||||
|         *, | ||||
|         login_version: int | None = None, | ||||
|         https: bool | None = None, | ||||
|         http_port: int | None = None, | ||||
|     ) -> DeviceConnectionParameters: | ||||
|         """Return connection parameters from string values.""" | ||||
|         try: | ||||
| @@ -115,6 +118,7 @@ class DeviceConnectionParameters(_DeviceConfigBaseMixin): | ||||
|                 DeviceEncryptionType(encryption_type), | ||||
|                 login_version, | ||||
|                 https, | ||||
|                 http_port=http_port, | ||||
|             ) | ||||
|         except (ValueError, TypeError) as ex: | ||||
|             raise KasaException( | ||||
|   | ||||
| @@ -146,6 +146,7 @@ class ConnectAttempt(NamedTuple): | ||||
|     protocol: type | ||||
|     transport: type | ||||
|     device: type | ||||
|     https: bool | ||||
|  | ||||
|  | ||||
| class DiscoveredMeta(TypedDict): | ||||
| @@ -637,10 +638,10 @@ class Discover: | ||||
|             Device.Family.IotIpCamera, | ||||
|         } | ||||
|         candidates: dict[ | ||||
|             tuple[type[BaseProtocol], type[BaseTransport], type[Device]], | ||||
|             tuple[type[BaseProtocol], type[BaseTransport], type[Device], bool], | ||||
|             tuple[BaseProtocol, DeviceConfig], | ||||
|         ] = { | ||||
|             (type(protocol), type(protocol._transport), device_class): ( | ||||
|             (type(protocol), type(protocol._transport), device_class, https): ( | ||||
|                 protocol, | ||||
|                 config, | ||||
|             ) | ||||
| @@ -870,8 +871,9 @@ class Discover: | ||||
|             config.connection_type = DeviceConnectionParameters.from_values( | ||||
|                 type_, | ||||
|                 encrypt_type, | ||||
|                 login_version, | ||||
|                 encrypt_schm.is_support_https, | ||||
|                 login_version=login_version, | ||||
|                 https=encrypt_schm.is_support_https, | ||||
|                 http_port=encrypt_schm.http_port, | ||||
|             ) | ||||
|         except KasaException as ex: | ||||
|             raise UnsupportedDeviceError( | ||||
|   | ||||
| @@ -36,6 +36,18 @@ if TYPE_CHECKING: | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| def _mask_area_list(area_list: list[dict[str, Any]]) -> list[dict[str, Any]]: | ||||
|     def mask_area(area: dict[str, Any]) -> dict[str, Any]: | ||||
|         result = {**area} | ||||
|         # Will leave empty names as blank | ||||
|         if area.get("name"): | ||||
|             result["name"] = "I01BU0tFRF9OQU1FIw=="  # #MASKED_NAME# | ||||
|         return result | ||||
|  | ||||
|     return [mask_area(area) for area in area_list] | ||||
|  | ||||
|  | ||||
| REDACTORS: dict[str, Callable[[Any], Any] | None] = { | ||||
|     "latitude": lambda x: 0, | ||||
|     "longitude": lambda x: 0, | ||||
| @@ -71,6 +83,10 @@ REDACTORS: dict[str, Callable[[Any], Any] | None] = { | ||||
|     "custom_sn": lambda _: "000000000000", | ||||
|     "location": lambda x: "#MASKED_NAME#" if x else "", | ||||
|     "map_data": lambda x: "#SCRUBBED_MAPDATA#" if x else "", | ||||
|     "map_name": lambda x: "I01BU0tFRF9OQU1FIw==",  # #MASKED_NAME# | ||||
|     "area_list": _mask_area_list, | ||||
|     # unknown robovac binary blob in get_device_info | ||||
|     "cd": lambda x: "I01BU0tFRF9CSU5BUlkj",  # #MASKED_BINARY# | ||||
| } | ||||
|  | ||||
| # Queries that are known not to work properly when sent as a | ||||
|   | ||||
| @@ -120,6 +120,8 @@ class AesTransport(BaseTransport): | ||||
|     @property | ||||
|     def default_port(self) -> int: | ||||
|         """Default port for the transport.""" | ||||
|         if port := self._config.connection_type.http_port: | ||||
|             return port | ||||
|         return self.DEFAULT_PORT | ||||
|  | ||||
|     @property | ||||
|   | ||||
| @@ -48,6 +48,7 @@ import datetime | ||||
| import hashlib | ||||
| import logging | ||||
| import secrets | ||||
| import ssl | ||||
| import struct | ||||
| import time | ||||
| from asyncio import Future | ||||
| @@ -92,8 +93,21 @@ class KlapTransport(BaseTransport): | ||||
|     """ | ||||
|  | ||||
|     DEFAULT_PORT: int = 80 | ||||
|     DEFAULT_HTTPS_PORT: int = 4433 | ||||
|  | ||||
|     SESSION_COOKIE_NAME = "TP_SESSIONID" | ||||
|     TIMEOUT_COOKIE_NAME = "TIMEOUT" | ||||
|     # Copy & paste from sslaestransport | ||||
|     CIPHERS = ":".join( | ||||
|         [ | ||||
|             "AES256-GCM-SHA384", | ||||
|             "AES256-SHA256", | ||||
|             "AES128-GCM-SHA256", | ||||
|             "AES128-SHA256", | ||||
|             "AES256-SHA", | ||||
|         ] | ||||
|     ) | ||||
|     _ssl_context: ssl.SSLContext | None = None | ||||
|  | ||||
|     def __init__( | ||||
|         self, | ||||
| @@ -125,12 +139,20 @@ class KlapTransport(BaseTransport): | ||||
|         self._session_cookie: dict[str, Any] | None = None | ||||
|  | ||||
|         _LOGGER.debug("Created KLAP transport for %s", self._host) | ||||
|         self._app_url = URL(f"http://{self._host}:{self._port}/app") | ||||
|         protocol = "https" if config.connection_type.https else "http" | ||||
|         self._app_url = URL(f"{protocol}://{self._host}:{self._port}/app") | ||||
|         self._request_url = self._app_url / "request" | ||||
|  | ||||
|     @property | ||||
|     def default_port(self) -> int: | ||||
|         """Default port for the transport.""" | ||||
|         config = self._config | ||||
|         if port := config.connection_type.http_port: | ||||
|             return port | ||||
|  | ||||
|         if config.connection_type.https: | ||||
|             return self.DEFAULT_HTTPS_PORT | ||||
|  | ||||
|         return self.DEFAULT_PORT | ||||
|  | ||||
|     @property | ||||
| @@ -152,7 +174,9 @@ class KlapTransport(BaseTransport): | ||||
|  | ||||
|         url = self._app_url / "handshake1" | ||||
|  | ||||
|         response_status, response_data = await self._http_client.post(url, data=payload) | ||||
|         response_status, response_data = await self._http_client.post( | ||||
|             url, data=payload, ssl=await self._get_ssl_context() | ||||
|         ) | ||||
|  | ||||
|         if _LOGGER.isEnabledFor(logging.DEBUG): | ||||
|             _LOGGER.debug( | ||||
| @@ -263,6 +287,7 @@ class KlapTransport(BaseTransport): | ||||
|             url, | ||||
|             data=payload, | ||||
|             cookies_dict=self._session_cookie, | ||||
|             ssl=await self._get_ssl_context(), | ||||
|         ) | ||||
|  | ||||
|         if _LOGGER.isEnabledFor(logging.DEBUG): | ||||
| @@ -337,6 +362,7 @@ class KlapTransport(BaseTransport): | ||||
|             params={"seq": seq}, | ||||
|             data=payload, | ||||
|             cookies_dict=self._session_cookie, | ||||
|             ssl=await self._get_ssl_context(), | ||||
|         ) | ||||
|  | ||||
|         msg = ( | ||||
| @@ -413,6 +439,23 @@ class KlapTransport(BaseTransport): | ||||
|         un = creds.username | ||||
|         return md5(un.encode()) | ||||
|  | ||||
|     # Copy & paste from sslaestransport. | ||||
|     def _create_ssl_context(self) -> ssl.SSLContext: | ||||
|         context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) | ||||
|         context.set_ciphers(self.CIPHERS) | ||||
|         context.check_hostname = False | ||||
|         context.verify_mode = ssl.CERT_NONE | ||||
|         return context | ||||
|  | ||||
|     # Copy & paste from sslaestransport. | ||||
|     async def _get_ssl_context(self) -> ssl.SSLContext: | ||||
|         if not self._ssl_context: | ||||
|             loop = asyncio.get_running_loop() | ||||
|             self._ssl_context = await loop.run_in_executor( | ||||
|                 None, self._create_ssl_context | ||||
|             ) | ||||
|         return self._ssl_context | ||||
|  | ||||
|  | ||||
| class KlapTransportV2(KlapTransport): | ||||
|     """Implementation of the KLAP encryption protocol with v2 hanshake hashes.""" | ||||
|   | ||||
| @@ -55,6 +55,8 @@ class LinkieTransportV2(BaseTransport): | ||||
|     @property | ||||
|     def default_port(self) -> int: | ||||
|         """Default port for the transport.""" | ||||
|         if port := self._config.connection_type.http_port: | ||||
|             return port | ||||
|         return self.DEFAULT_PORT | ||||
|  | ||||
|     @property | ||||
|   | ||||
| @@ -133,6 +133,8 @@ class SslAesTransport(BaseTransport): | ||||
|     @property | ||||
|     def default_port(self) -> int: | ||||
|         """Default port for the transport.""" | ||||
|         if port := self._config.connection_type.http_port: | ||||
|             return port | ||||
|         return self.DEFAULT_PORT | ||||
|  | ||||
|     @staticmethod | ||||
|   | ||||
| @@ -94,6 +94,8 @@ class SslTransport(BaseTransport): | ||||
|     @property | ||||
|     def default_port(self) -> int: | ||||
|         """Default port for the transport.""" | ||||
|         if port := self._config.connection_type.http_port: | ||||
|             return port | ||||
|         return self.DEFAULT_PORT | ||||
|  | ||||
|     @property | ||||
|   | ||||
| @@ -159,6 +159,7 @@ def create_discovery_mock(ip: str, fixture_data: dict): | ||||
|         https: bool | ||||
|         login_version: int | None = None | ||||
|         port_override: int | None = None | ||||
|         http_port: int | None = None | ||||
|  | ||||
|         @property | ||||
|         def model(self) -> str: | ||||
| @@ -194,9 +195,15 @@ def create_discovery_mock(ip: str, fixture_data: dict): | ||||
|         ): | ||||
|             login_version = max([int(i) for i in et]) | ||||
|         https = discovery_result["mgt_encrypt_schm"]["is_support_https"] | ||||
|         http_port = discovery_result["mgt_encrypt_schm"].get("http_port") | ||||
|         if not http_port:  # noqa: SIM108 | ||||
|             # Not all discovery responses set the http port, i.e. smartcam. | ||||
|             default_port = 443 if https else 80 | ||||
|         else: | ||||
|             default_port = http_port | ||||
|         dm = _DiscoveryMock( | ||||
|             ip, | ||||
|             80, | ||||
|             default_port, | ||||
|             20002, | ||||
|             discovery_data, | ||||
|             fixture_data, | ||||
| @@ -204,6 +211,7 @@ def create_discovery_mock(ip: str, fixture_data: dict): | ||||
|             encrypt_type, | ||||
|             https, | ||||
|             login_version, | ||||
|             http_port=http_port, | ||||
|         ) | ||||
|     else: | ||||
|         sys_info = fixture_data["system"]["get_sysinfo"] | ||||
|   | ||||
							
								
								
									
										888
									
								
								tests/fixtures/smart/RV30 Max(US)_1.0_1.2.0.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										888
									
								
								tests/fixtures/smart/RV30 Max(US)_1.0_1.2.0.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,888 @@ | ||||
| { | ||||
|     "component_nego": { | ||||
|         "component_list": [ | ||||
|             { | ||||
|                 "id": "device", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "iot_cloud", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "time", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "firmware", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "quick_setup", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "clean", | ||||
|                 "ver_code": 3 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "battery", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "consumables", | ||||
|                 "ver_code": 2 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "direction_control", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "button_and_led", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "speaker", | ||||
|                 "ver_code": 3 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "schedule", | ||||
|                 "ver_code": 3 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "wireless", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "map", | ||||
|                 "ver_code": 2 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "auto_change_map", | ||||
|                 "ver_code": 2 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "mop", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "ble_whole_setup", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "do_not_disturb", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "inherit", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "device_local_time", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "charge_pose_clean", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "continue_breakpoint_sweep", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "goto_point", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "furniture", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "map_cloud_backup", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "dev_log", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "map_lock", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "carpet_area", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "clean_angle", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "clean_percent", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "no_pose_config", | ||||
|                 "ver_code": 1 | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     "discovery_result": { | ||||
|         "error_code": 0, | ||||
|         "result": { | ||||
|             "device_id": "00000000000000000000000000000000", | ||||
|             "device_model": "RV30 Max(US)", | ||||
|             "device_type": "SMART.TAPOROBOVAC", | ||||
|             "factory_default": false, | ||||
|             "ip": "127.0.0.123", | ||||
|             "is_support_iot_cloud": true, | ||||
|             "mac": "7C-F1-7E-00-00-00", | ||||
|             "mgt_encrypt_schm": { | ||||
|                 "encrypt_type": "KLAP", | ||||
|                 "http_port": 4433, | ||||
|                 "is_support_https": true | ||||
|             }, | ||||
|             "obd_src": "tplink", | ||||
|             "owner": "00000000000000000000000000000000", | ||||
|             "protocol_version": 1 | ||||
|         } | ||||
|     }, | ||||
|     "getAreaUnit": { | ||||
|         "area_unit": 1 | ||||
|     }, | ||||
|     "getAutoChangeMap": { | ||||
|         "auto_change_map": true | ||||
|     }, | ||||
|     "getBatteryInfo": { | ||||
|         "battery_percentage": 100 | ||||
|     }, | ||||
|     "getCarpetClean": { | ||||
|         "carpet_clean_prefer": "boost" | ||||
|     }, | ||||
|     "getChildLockInfo": { | ||||
|         "child_lock_status": false | ||||
|     }, | ||||
|     "getCleanAttr": { | ||||
|         "cistern": 1, | ||||
|         "clean_number": 1, | ||||
|         "suction": 2 | ||||
|     }, | ||||
|     "getCleanInfo": { | ||||
|         "clean_area": 59, | ||||
|         "clean_percent": 100, | ||||
|         "clean_time": 56 | ||||
|     }, | ||||
|     "getCleanRecords": { | ||||
|         "lastest_day_record": [ | ||||
|             1737387294, | ||||
|             56, | ||||
|             59, | ||||
|             1 | ||||
|         ], | ||||
|         "record_list": [ | ||||
|             { | ||||
|                 "clean_area": 59, | ||||
|                 "clean_time": 57, | ||||
|                 "dust_collection": false, | ||||
|                 "error": 0, | ||||
|                 "info_num": 0, | ||||
|                 "map_id": 1734727686, | ||||
|                 "message": 0, | ||||
|                 "record_index": 0, | ||||
|                 "start_type": 4, | ||||
|                 "task_type": 0, | ||||
|                 "timestamp": 1737041654 | ||||
|             }, | ||||
|             { | ||||
|                 "clean_area": 39, | ||||
|                 "clean_time": 58, | ||||
|                 "dust_collection": false, | ||||
|                 "error": 0, | ||||
|                 "info_num": 1, | ||||
|                 "map_id": 1736541042, | ||||
|                 "message": 0, | ||||
|                 "record_index": 1, | ||||
|                 "start_type": 1, | ||||
|                 "task_type": 0, | ||||
|                 "timestamp": 1737055944 | ||||
|             }, | ||||
|             { | ||||
|                 "clean_area": 1, | ||||
|                 "clean_time": 3, | ||||
|                 "dust_collection": false, | ||||
|                 "error": 0, | ||||
|                 "info_num": 0, | ||||
|                 "map_id": 1734727686, | ||||
|                 "message": 0, | ||||
|                 "record_index": 2, | ||||
|                 "start_type": 1, | ||||
|                 "task_type": 4, | ||||
|                 "timestamp": 1737074472 | ||||
|             }, | ||||
|             { | ||||
|                 "clean_area": 59, | ||||
|                 "clean_time": 58, | ||||
|                 "dust_collection": false, | ||||
|                 "error": 0, | ||||
|                 "info_num": 0, | ||||
|                 "map_id": 1734727686, | ||||
|                 "message": 0, | ||||
|                 "record_index": 3, | ||||
|                 "start_type": 4, | ||||
|                 "task_type": 0, | ||||
|                 "timestamp": 1737128195 | ||||
|             }, | ||||
|             { | ||||
|                 "clean_area": 68, | ||||
|                 "clean_time": 78, | ||||
|                 "dust_collection": false, | ||||
|                 "error": 0, | ||||
|                 "info_num": 2, | ||||
|                 "map_id": 1736541042, | ||||
|                 "message": 0, | ||||
|                 "record_index": 4, | ||||
|                 "start_type": 1, | ||||
|                 "task_type": 1, | ||||
|                 "timestamp": 1737216716 | ||||
|             }, | ||||
|             { | ||||
|                 "clean_area": 3, | ||||
|                 "clean_time": 3, | ||||
|                 "dust_collection": false, | ||||
|                 "error": 0, | ||||
|                 "info_num": 0, | ||||
|                 "map_id": 1734742958, | ||||
|                 "message": 0, | ||||
|                 "record_index": 5, | ||||
|                 "start_type": 1, | ||||
|                 "task_type": 3, | ||||
|                 "timestamp": 1737300731 | ||||
|             }, | ||||
|             { | ||||
|                 "clean_area": 20, | ||||
|                 "clean_time": 16, | ||||
|                 "dust_collection": false, | ||||
|                 "error": 0, | ||||
|                 "info_num": 0, | ||||
|                 "map_id": 1734742958, | ||||
|                 "message": 0, | ||||
|                 "record_index": 6, | ||||
|                 "start_type": 1, | ||||
|                 "task_type": 3, | ||||
|                 "timestamp": 1737304391 | ||||
|             }, | ||||
|             { | ||||
|                 "clean_area": 59, | ||||
|                 "clean_time": 56, | ||||
|                 "dust_collection": false, | ||||
|                 "error": 0, | ||||
|                 "info_num": 0, | ||||
|                 "map_id": 1734727686, | ||||
|                 "message": 0, | ||||
|                 "record_index": 7, | ||||
|                 "start_type": 4, | ||||
|                 "task_type": 0, | ||||
|                 "timestamp": 1737387294 | ||||
|             }, | ||||
|             { | ||||
|                 "clean_area": 17, | ||||
|                 "clean_time": 16, | ||||
|                 "dust_collection": false, | ||||
|                 "error": 0, | ||||
|                 "info_num": 0, | ||||
|                 "map_id": 1734727686, | ||||
|                 "message": 0, | ||||
|                 "record_index": 8, | ||||
|                 "start_type": 1, | ||||
|                 "task_type": 3, | ||||
|                 "timestamp": 1736707487 | ||||
|             }, | ||||
|             { | ||||
|                 "clean_area": 8, | ||||
|                 "clean_time": 10, | ||||
|                 "dust_collection": false, | ||||
|                 "error": 0, | ||||
|                 "info_num": 0, | ||||
|                 "map_id": 1734727686, | ||||
|                 "message": 0, | ||||
|                 "record_index": 9, | ||||
|                 "start_type": 1, | ||||
|                 "task_type": 4, | ||||
|                 "timestamp": 1736708425 | ||||
|             }, | ||||
|             { | ||||
|                 "clean_area": 59, | ||||
|                 "clean_time": 54, | ||||
|                 "dust_collection": false, | ||||
|                 "error": 0, | ||||
|                 "info_num": 0, | ||||
|                 "map_id": 1734727686, | ||||
|                 "message": 0, | ||||
|                 "record_index": 10, | ||||
|                 "start_type": 4, | ||||
|                 "task_type": 0, | ||||
|                 "timestamp": 1736782261 | ||||
|             }, | ||||
|             { | ||||
|                 "clean_area": 60, | ||||
|                 "clean_time": 56, | ||||
|                 "dust_collection": false, | ||||
|                 "error": 0, | ||||
|                 "info_num": 0, | ||||
|                 "map_id": 1734727686, | ||||
|                 "message": 0, | ||||
|                 "record_index": 11, | ||||
|                 "start_type": 4, | ||||
|                 "task_type": 0, | ||||
|                 "timestamp": 1736868752 | ||||
|             }, | ||||
|             { | ||||
|                 "clean_area": 58, | ||||
|                 "clean_time": 68, | ||||
|                 "dust_collection": true, | ||||
|                 "error": 1, | ||||
|                 "info_num": 0, | ||||
|                 "map_id": 1736541042, | ||||
|                 "message": 0, | ||||
|                 "record_index": 12, | ||||
|                 "start_type": 1, | ||||
|                 "task_type": 1, | ||||
|                 "timestamp": 1736881428 | ||||
|             }, | ||||
|             { | ||||
|                 "clean_area": 59, | ||||
|                 "clean_time": 59, | ||||
|                 "dust_collection": false, | ||||
|                 "error": 0, | ||||
|                 "info_num": 0, | ||||
|                 "map_id": 1734727686, | ||||
|                 "message": 0, | ||||
|                 "record_index": 13, | ||||
|                 "start_type": 4, | ||||
|                 "task_type": 0, | ||||
|                 "timestamp": 1736955682 | ||||
|             }, | ||||
|             { | ||||
|                 "clean_area": 36, | ||||
|                 "clean_time": 33, | ||||
|                 "dust_collection": false, | ||||
|                 "error": 0, | ||||
|                 "info_num": 0, | ||||
|                 "map_id": 1734727686, | ||||
|                 "message": 0, | ||||
|                 "record_index": 14, | ||||
|                 "start_type": 1, | ||||
|                 "task_type": 4, | ||||
|                 "timestamp": 1736960713 | ||||
|             } | ||||
|         ], | ||||
|         "record_list_num": 15, | ||||
|         "total_area": 2304, | ||||
|         "total_number": 85, | ||||
|         "total_time": 2510 | ||||
|     }, | ||||
|     "getCleanStatus": { | ||||
|         "clean_status": 0, | ||||
|         "is_mapping": false, | ||||
|         "is_relocating": false, | ||||
|         "is_working": false | ||||
|     }, | ||||
|     "getConsumablesInfo": { | ||||
|         "charge_contact_time": 660, | ||||
|         "edge_brush_time": 2743, | ||||
|         "filter_time": 287, | ||||
|         "main_brush_lid_time": 2462, | ||||
|         "rag_time": 0, | ||||
|         "roll_brush_time": 2719, | ||||
|         "sensor_time": 935 | ||||
|     }, | ||||
|     "getCurrentVoiceLanguage": { | ||||
|         "name": "bb053ca2c5605a55090fcdb952f3902b", | ||||
|         "version": 2 | ||||
|     }, | ||||
|     "getDoNotDisturb": { | ||||
|         "do_not_disturb": true, | ||||
|         "e_min": 480, | ||||
|         "s_min": 1320 | ||||
|     }, | ||||
|     "getMapData": { | ||||
|         "area_list": [ | ||||
|             { | ||||
|                 "cistern": 1, | ||||
|                 "clean_number": 1, | ||||
|                 "color": 3, | ||||
|                 "floor_texture": -1, | ||||
|                 "id": 5, | ||||
|                 "name": "I01BU0tFRF9OQU1FIw==", | ||||
|                 "suction": 2, | ||||
|                 "type": "room" | ||||
|             }, | ||||
|             { | ||||
|                 "cistern": 1, | ||||
|                 "clean_number": 1, | ||||
|                 "color": 4, | ||||
|                 "floor_texture": -1, | ||||
|                 "id": 6, | ||||
|                 "name": "I01BU0tFRF9OQU1FIw==", | ||||
|                 "suction": 2, | ||||
|                 "type": "room" | ||||
|             }, | ||||
|             { | ||||
|                 "cistern": 1, | ||||
|                 "clean_number": 1, | ||||
|                 "color": 1, | ||||
|                 "floor_texture": 0, | ||||
|                 "id": 2, | ||||
|                 "name": "I01BU0tFRF9OQU1FIw==", | ||||
|                 "suction": 2, | ||||
|                 "type": "room" | ||||
|             }, | ||||
|             { | ||||
|                 "cistern": 1, | ||||
|                 "clean_number": 1, | ||||
|                 "color": 5, | ||||
|                 "floor_texture": 90, | ||||
|                 "id": 3, | ||||
|                 "name": "I01BU0tFRF9OQU1FIw==", | ||||
|                 "suction": 2, | ||||
|                 "type": "room" | ||||
|             }, | ||||
|             { | ||||
|                 "cistern": 1, | ||||
|                 "clean_number": 1, | ||||
|                 "color": 2, | ||||
|                 "floor_texture": -1, | ||||
|                 "id": 4, | ||||
|                 "name": "I01BU0tFRF9OQU1FIw==", | ||||
|                 "suction": 2, | ||||
|                 "type": "room" | ||||
|             }, | ||||
|             { | ||||
|                 "id": 401, | ||||
|                 "type": "virtual_wall", | ||||
|                 "vertexs": [ | ||||
|                     [ | ||||
|                         4711, | ||||
|                         985 | ||||
|                     ], | ||||
|                     [ | ||||
|                         4717, | ||||
|                         -404 | ||||
|                     ] | ||||
|                 ] | ||||
|             }, | ||||
|             { | ||||
|                 "id": 301, | ||||
|                 "type": "forbid", | ||||
|                 "vertexs": [ | ||||
|                     [ | ||||
|                         3061, | ||||
|                         -3027 | ||||
|                     ], | ||||
|                     [ | ||||
|                         3580, | ||||
|                         -3027 | ||||
|                     ], | ||||
|                     [ | ||||
|                         3580, | ||||
|                         -3692 | ||||
|                     ], | ||||
|                     [ | ||||
|                         3061, | ||||
|                         -3692 | ||||
|                     ] | ||||
|                 ] | ||||
|             }, | ||||
|             { | ||||
|                 "id": 402, | ||||
|                 "type": "virtual_wall", | ||||
|                 "vertexs": [ | ||||
|                     [ | ||||
|                         5302, | ||||
|                         6816 | ||||
|                     ], | ||||
|                     [ | ||||
|                         5304, | ||||
|                         4924 | ||||
|                     ] | ||||
|                 ] | ||||
|             }, | ||||
|             { | ||||
|                 "cistern": -1, | ||||
|                 "clean_number": 1, | ||||
|                 "id": 501, | ||||
|                 "suction": -1, | ||||
|                 "type": "area", | ||||
|                 "vertexs": [ | ||||
|                     [ | ||||
|                         2889, | ||||
|                         6241 | ||||
|                     ], | ||||
|                     [ | ||||
|                         3721, | ||||
|                         6241 | ||||
|                     ], | ||||
|                     [ | ||||
|                         3721, | ||||
|                         4919 | ||||
|                     ], | ||||
|                     [ | ||||
|                         2889, | ||||
|                         4919 | ||||
|                     ] | ||||
|                 ] | ||||
|             }, | ||||
|             { | ||||
|                 "carpet_strategy": 11, | ||||
|                 "id": 101, | ||||
|                 "type": "carpet_rectangle", | ||||
|                 "vertexs": [ | ||||
|                     [ | ||||
|                         20, | ||||
|                         -2012 | ||||
|                     ], | ||||
|                     [ | ||||
|                         2857, | ||||
|                         -2012 | ||||
|                     ], | ||||
|                     [ | ||||
|                         2857, | ||||
|                         -4122 | ||||
|                     ], | ||||
|                     [ | ||||
|                         20, | ||||
|                         -4122 | ||||
|                     ] | ||||
|                 ] | ||||
|             }, | ||||
|             { | ||||
|                 "carpet_strategy": 11, | ||||
|                 "id": 102, | ||||
|                 "type": "carpet_rectangle", | ||||
|                 "vertexs": [ | ||||
|                     [ | ||||
|                         1327, | ||||
|                         3064 | ||||
|                     ], | ||||
|                     [ | ||||
|                         2428, | ||||
|                         3064 | ||||
|                     ], | ||||
|                     [ | ||||
|                         2428, | ||||
|                         2258 | ||||
|                     ], | ||||
|                     [ | ||||
|                         1327, | ||||
|                         2258 | ||||
|                     ] | ||||
|                 ] | ||||
|             }, | ||||
|             { | ||||
|                 "carpet_strategy": 11, | ||||
|                 "id": 103, | ||||
|                 "type": "carpet_rectangle", | ||||
|                 "vertexs": [ | ||||
|                     [ | ||||
|                         4458, | ||||
|                         5974 | ||||
|                     ], | ||||
|                     [ | ||||
|                         5336, | ||||
|                         5974 | ||||
|                     ], | ||||
|                     [ | ||||
|                         5336, | ||||
|                         4903 | ||||
|                     ], | ||||
|                     [ | ||||
|                         4458, | ||||
|                         4903 | ||||
|                     ] | ||||
|                 ] | ||||
|             }, | ||||
|             { | ||||
|                 "carpet_strategy": 11, | ||||
|                 "id": 104, | ||||
|                 "type": "carpet_rectangle", | ||||
|                 "vertexs": [ | ||||
|                     [ | ||||
|                         -1383, | ||||
|                         2730 | ||||
|                     ], | ||||
|                     [ | ||||
|                         -761, | ||||
|                         2730 | ||||
|                     ], | ||||
|                     [ | ||||
|                         -761, | ||||
|                         1587 | ||||
|                     ], | ||||
|                     [ | ||||
|                         -1383, | ||||
|                         1587 | ||||
|                     ] | ||||
|                 ] | ||||
|             } | ||||
|         ], | ||||
|         "auto_area_flag": true, | ||||
|         "bit_list": { | ||||
|             "auto_area": [ | ||||
|                 0, | ||||
|                 100 | ||||
|             ], | ||||
|             "barrier": 0, | ||||
|             "clean": 255, | ||||
|             "none": 127 | ||||
|         }, | ||||
|         "bitnum": 8, | ||||
|         "charge_coor": [ | ||||
|             65, | ||||
|             134, | ||||
|             272 | ||||
|         ], | ||||
|         "furniture_list": [], | ||||
|         "height": 303, | ||||
|         "map_data": "#SCRUBBED_MAPDATA#", | ||||
|         "map_hash": "A5D8FA4487CC40312EF58D8123F0A4CC", | ||||
|         "map_id": 1734727686, | ||||
|         "map_locked": 0, | ||||
|         "map_name": "I01BU0tFRF9OQU1FIw==", | ||||
|         "origin_coor": [ | ||||
|             -33, | ||||
|             -108, | ||||
|             270 | ||||
|         ], | ||||
|         "path_id": 122, | ||||
|         "pix_len": 66660, | ||||
|         "pix_lz4len": 6826, | ||||
|         "real_charge_coor": [ | ||||
|             1599, | ||||
|             1295, | ||||
|             272 | ||||
|         ], | ||||
|         "real_origin_coor": [ | ||||
|             -1674, | ||||
|             -5424, | ||||
|             270 | ||||
|         ], | ||||
|         "real_vac_coor": [ | ||||
|             1599, | ||||
|             1076, | ||||
|             272 | ||||
|         ], | ||||
|         "resolution": 50, | ||||
|         "resolution_unit": "mm", | ||||
|         "vac_coor": [ | ||||
|             65, | ||||
|             130, | ||||
|             272 | ||||
|         ], | ||||
|         "version": "LDS", | ||||
|         "width": 220 | ||||
|     }, | ||||
|     "getMapInfo": { | ||||
|         "auto_change_map": true, | ||||
|         "current_map_id": 1734727686, | ||||
|         "map_list": [ | ||||
|             { | ||||
|                 "auto_area_flag": true, | ||||
|                 "global_cleaned": -1, | ||||
|                 "is_saved": true, | ||||
|                 "map_id": 1734727686, | ||||
|                 "map_locked": 0, | ||||
|                 "map_name": "I01BU0tFRF9OQU1FIw==", | ||||
|                 "rotate_angle": 270, | ||||
|                 "update_time": 1737387285 | ||||
|             }, | ||||
|             { | ||||
|                 "auto_area_flag": true, | ||||
|                 "global_cleaned": -1, | ||||
|                 "is_saved": true, | ||||
|                 "map_id": 1734742958, | ||||
|                 "map_locked": 0, | ||||
|                 "map_name": "I01BU0tFRF9OQU1FIw==", | ||||
|                 "rotate_angle": 0, | ||||
|                 "update_time": 1737304392 | ||||
|             }, | ||||
|             { | ||||
|                 "auto_area_flag": true, | ||||
|                 "global_cleaned": -1, | ||||
|                 "is_saved": true, | ||||
|                 "map_id": 1736541042, | ||||
|                 "map_locked": 0, | ||||
|                 "map_name": "I01BU0tFRF9OQU1FIw==", | ||||
|                 "rotate_angle": 270, | ||||
|                 "update_time": 1737216718 | ||||
|             } | ||||
|         ], | ||||
|         "map_num": 3, | ||||
|         "version": "LDS" | ||||
|     }, | ||||
|     "getMopState": { | ||||
|         "mop_state": false | ||||
|     }, | ||||
|     "getVacStatus": { | ||||
|         "err_status": [ | ||||
|             0 | ||||
|         ], | ||||
|         "errorCode_id": [ | ||||
|             1144500830 | ||||
|         ], | ||||
|         "prompt": [], | ||||
|         "promptCode_id": [], | ||||
|         "status": 6 | ||||
|     }, | ||||
|     "getVolume": { | ||||
|         "volume": 60 | ||||
|     }, | ||||
|     "get_device_info": { | ||||
|         "auto_pack_ver": "0.0.131.1852", | ||||
|         "avatar": "", | ||||
|         "board_sn": "000000000000", | ||||
|         "cd": "I01BU0tFRF9CSU5BUlkj", | ||||
|         "custom_sn": "000000000000", | ||||
|         "device_id": "0000000000000000000000000000000000000000", | ||||
|         "fw_id": "00000000000000000000000000000000", | ||||
|         "fw_ver": "1.2.0 Build 241219 Rel.163928", | ||||
|         "has_set_location_info": true, | ||||
|         "hw_id": "00000000000000000000000000000000", | ||||
|         "hw_ver": "1.0", | ||||
|         "ip": "127.0.0.123", | ||||
|         "lang": "", | ||||
|         "latitude": 0, | ||||
|         "linux_ver": "V21.198.1708420747", | ||||
|         "location": "", | ||||
|         "longitude": 0, | ||||
|         "mac": "7C-F1-7E-00-00-00", | ||||
|         "mcu_ver": "1.1.2724.442", | ||||
|         "model": "RV30 Max", | ||||
|         "nickname": "I01BU0tFRF9OQU1FIw==", | ||||
|         "oem_id": "00000000000000000000000000000000", | ||||
|         "overheated": false, | ||||
|         "product_id": "1794", | ||||
|         "region": "America/Chicago", | ||||
|         "rssi": -38, | ||||
|         "signal_level": 3, | ||||
|         "specs": "", | ||||
|         "ssid": "I01BU0tFRF9TU0lEIw==", | ||||
|         "sub_ver": "0.0.131.1852-1.4.40", | ||||
|         "time_diff": -360, | ||||
|         "total_ver": "1.4.40", | ||||
|         "type": "SMART.TAPOROBOVAC" | ||||
|     }, | ||||
|     "get_device_time": { | ||||
|         "region": "America/Chicago", | ||||
|         "time_diff": -360, | ||||
|         "timestamp": 1737399953 | ||||
|     }, | ||||
|     "get_fw_download_state": { | ||||
|         "auto_upgrade": false, | ||||
|         "download_progress": 0, | ||||
|         "reboot_time": 5, | ||||
|         "status": 0, | ||||
|         "upgrade_time": 5 | ||||
|     }, | ||||
|     "get_inherit_info": { | ||||
|         "inherit_status": true | ||||
|     }, | ||||
|     "get_latest_fw": { | ||||
|         "fw_size": 0, | ||||
|         "fw_ver": "1.2.0 Build 241219 Rel.163928", | ||||
|         "hw_id": "", | ||||
|         "need_to_upgrade": false, | ||||
|         "oem_id": "", | ||||
|         "release_date": "", | ||||
|         "release_note": "", | ||||
|         "type": 0 | ||||
|     }, | ||||
|     "get_schedule_rules": { | ||||
|         "enable": true, | ||||
|         "rule_list": [ | ||||
|             { | ||||
|                 "alarm_min": 0, | ||||
|                 "cancel": false, | ||||
|                 "clean_attr": { | ||||
|                     "cistern": 2, | ||||
|                     "clean_mode": 0, | ||||
|                     "clean_number": 1, | ||||
|                     "clean_order": false, | ||||
|                     "suction": 2 | ||||
|                 }, | ||||
|                 "day": 21, | ||||
|                 "enable": true, | ||||
|                 "id": "S1", | ||||
|                 "invalid": 0, | ||||
|                 "mode": "repeat", | ||||
|                 "month": 1, | ||||
|                 "s_min": 515, | ||||
|                 "start_remind": true, | ||||
|                 "week_day": 62, | ||||
|                 "year": 2025 | ||||
|             } | ||||
|         ], | ||||
|         "schedule_rule_max_count": 32, | ||||
|         "start_index": 0, | ||||
|         "sum": 1 | ||||
|     }, | ||||
|     "get_wireless_scan_info": { | ||||
|         "ap_list": [ | ||||
|             { | ||||
|                 "key_type": "wpa2_psk", | ||||
|                 "signal_level": 3, | ||||
|                 "ssid": "I01BU0tFRF9TU0lEIw==" | ||||
|             }, | ||||
|             { | ||||
|                 "key_type": "wpa2_psk", | ||||
|                 "signal_level": 1, | ||||
|                 "ssid": "I01BU0tFRF9TU0lEIw==" | ||||
|             }, | ||||
|             { | ||||
|                 "key_type": "wpa2_psk", | ||||
|                 "signal_level": 1, | ||||
|                 "ssid": "I01BU0tFRF9TU0lEIw==" | ||||
|             }, | ||||
|             { | ||||
|                 "key_type": "wpa2_psk", | ||||
|                 "signal_level": 1, | ||||
|                 "ssid": "I01BU0tFRF9TU0lEIw==" | ||||
|             }, | ||||
|             { | ||||
|                 "key_type": "wpa2_psk", | ||||
|                 "signal_level": 1, | ||||
|                 "ssid": "I01BU0tFRF9TU0lEIw==" | ||||
|             } | ||||
|         ], | ||||
|         "start_index": 0, | ||||
|         "sum": 5, | ||||
|         "wep_supported": true | ||||
|     }, | ||||
|     "qs_component_nego": { | ||||
|         "component_list": [ | ||||
|             { | ||||
|                 "id": "quick_setup", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "iot_cloud", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "firmware", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "ble_whole_setup", | ||||
|                 "ver_code": 1 | ||||
|             }, | ||||
|             { | ||||
|                 "id": "inherit", | ||||
|                 "ver_code": 1 | ||||
|             } | ||||
|         ], | ||||
|         "extra_info": { | ||||
|             "device_model": "RV30 Max", | ||||
|             "device_type": "SMART.TAPOROBOVAC" | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -117,6 +117,10 @@ async def test_actions( | ||||
| async def test_post_update_hook(dev: SmartDevice, err_status: list, error: ErrorCode): | ||||
|     """Test that post update hook sets error states correctly.""" | ||||
|     clean = next(get_parent_and_child_modules(dev, Module.Clean)) | ||||
|     assert clean | ||||
|  | ||||
|     # _post_update_hook will pop an item off the status list so create a copy. | ||||
|     err_status = [e for e in err_status] | ||||
|     clean.data["getVacStatus"]["err_status"] = err_status | ||||
|  | ||||
|     await clean._post_update_hook() | ||||
|   | ||||
| @@ -1308,11 +1308,11 @@ async def test_discover_config(dev: Device, mocker, runner): | ||||
|     expected = f"--device-family {cparam.device_family.value} --encrypt-type {cparam.encryption_type.value} {'--https' if cparam.https else '--no-https'}" | ||||
|     assert expected in res.output | ||||
|     assert re.search( | ||||
|         r"Attempt to connect to 127\.0\.0\.1 with \w+ \+ \w+ \+ \w+ failed", | ||||
|         r"Attempt to connect to 127\.0\.0\.1 with \w+ \+ \w+ \+ \w+ \+ \w+ failed", | ||||
|         res.output.replace("\n", ""), | ||||
|     ) | ||||
|     assert re.search( | ||||
|         r"Attempt to connect to 127\.0\.0\.1 with \w+ \+ \w+ \+ \w+ succeeded", | ||||
|         r"Attempt to connect to 127\.0\.0\.1 with \w+ \+ \w+ \+ \w+ \+ \w+ succeeded", | ||||
|         res.output.replace("\n", ""), | ||||
|     ) | ||||
|  | ||||
|   | ||||
| @@ -63,8 +63,9 @@ def _get_connection_type_device_class(discovery_info): | ||||
|         connection_type = DeviceConnectionParameters.from_values( | ||||
|             dr.device_type, | ||||
|             dr.mgt_encrypt_schm.encrypt_type, | ||||
|             dr.mgt_encrypt_schm.lv, | ||||
|             dr.mgt_encrypt_schm.is_support_https, | ||||
|             login_version=dr.mgt_encrypt_schm.lv, | ||||
|             https=dr.mgt_encrypt_schm.is_support_https, | ||||
|             http_port=dr.mgt_encrypt_schm.http_port, | ||||
|         ) | ||||
|     else: | ||||
|         connection_type = DeviceConnectionParameters.from_values( | ||||
|   | ||||
| @@ -157,14 +157,15 @@ async def test_discover_single(discovery_mock, custom_port, mocker): | ||||
|     ) | ||||
|     # Make sure discovery does not call update() | ||||
|     assert update_mock.call_count == 0 | ||||
|     if discovery_mock.default_port == 80: | ||||
|     if discovery_mock.default_port != 9999: | ||||
|         assert x.alias is None | ||||
|  | ||||
|     ct = DeviceConnectionParameters.from_values( | ||||
|         discovery_mock.device_type, | ||||
|         discovery_mock.encrypt_type, | ||||
|         discovery_mock.login_version, | ||||
|         discovery_mock.https, | ||||
|         login_version=discovery_mock.login_version, | ||||
|         https=discovery_mock.https, | ||||
|         http_port=discovery_mock.http_port, | ||||
|     ) | ||||
|     config = DeviceConfig( | ||||
|         host=host, | ||||
| @@ -425,9 +426,9 @@ async def test_discover_single_http_client(discovery_mock, mocker): | ||||
|  | ||||
|     x: Device = await Discover.discover_single(host) | ||||
|  | ||||
|     assert x.config.uses_http == (discovery_mock.default_port == 80) | ||||
|     assert x.config.uses_http == (discovery_mock.default_port != 9999) | ||||
|  | ||||
|     if discovery_mock.default_port == 80: | ||||
|     if discovery_mock.default_port != 9999: | ||||
|         assert x.protocol._transport._http_client.client != http_client | ||||
|         x.config.http_client = http_client | ||||
|         assert x.protocol._transport._http_client.client == http_client | ||||
| @@ -442,9 +443,9 @@ async def test_discover_http_client(discovery_mock, mocker): | ||||
|  | ||||
|     devices = await Discover.discover(discovery_timeout=0) | ||||
|     x: Device = devices[host] | ||||
|     assert x.config.uses_http == (discovery_mock.default_port == 80) | ||||
|     assert x.config.uses_http == (discovery_mock.default_port != 9999) | ||||
|  | ||||
|     if discovery_mock.default_port == 80: | ||||
|     if discovery_mock.default_port != 9999: | ||||
|         assert x.protocol._transport._http_client.client != http_client | ||||
|         x.config.http_client = http_client | ||||
|         assert x.protocol._transport._http_client.client == http_client | ||||
| @@ -674,8 +675,9 @@ async def test_discover_try_connect_all(discovery_mock, mocker): | ||||
|         cparams = DeviceConnectionParameters.from_values( | ||||
|             discovery_mock.device_type, | ||||
|             discovery_mock.encrypt_type, | ||||
|             discovery_mock.login_version, | ||||
|             discovery_mock.https, | ||||
|             login_version=discovery_mock.login_version, | ||||
|             https=discovery_mock.https, | ||||
|             http_port=discovery_mock.http_port, | ||||
|         ) | ||||
|         protocol = get_protocol( | ||||
|             DeviceConfig(discovery_mock.ip, connection_type=cparams) | ||||
| @@ -687,10 +689,13 @@ async def test_discover_try_connect_all(discovery_mock, mocker): | ||||
|         protocol_class = IotProtocol | ||||
|         transport_class = XorTransport | ||||
|  | ||||
|     default_port = discovery_mock.default_port | ||||
|  | ||||
|     async def _query(self, *args, **kwargs): | ||||
|         if ( | ||||
|             self.__class__ is protocol_class | ||||
|             and self._transport.__class__ is transport_class | ||||
|             and self._transport._port == default_port | ||||
|         ): | ||||
|             return discovery_mock.query_data | ||||
|         raise KasaException("Unable to execute query") | ||||
| @@ -699,6 +704,7 @@ async def test_discover_try_connect_all(discovery_mock, mocker): | ||||
|         if ( | ||||
|             self.protocol.__class__ is protocol_class | ||||
|             and self.protocol._transport.__class__ is transport_class | ||||
|             and self.protocol._transport._port == default_port | ||||
|         ): | ||||
|             return | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Teemu R.
					Teemu R.