diff --git a/roborock/device_features.py b/roborock/device_features.py index b2a3e34f..9c773336 100644 --- a/roborock/device_features.py +++ b/roborock/device_features.py @@ -539,6 +539,12 @@ class DeviceFeatures(RoborockBase): ] } ) + is_ai_recognition_setting_supported: bool = field( + metadata={"product_features": [ProductFeatures.AIRECOGNITION_SETTING]} + ) + is_ai_recognition_obstacle_supported: bool = field( + metadata={"product_features": [ProductFeatures.AIRECOGNITION_OBSTACLE]} + ) is_pure_clean_mop_supported: bool = field(metadata={"product_features": [ProductFeatures.CLEANMODE_PURECLEANMOP]}) is_new_remote_view_supported: bool = field(metadata={"product_features": [ProductFeatures.REMOTE_BACK]}) is_max_plus_mode_supported: bool = field(metadata={"product_features": [ProductFeatures.CLEANMODE_MAXPLUS]}) diff --git a/roborock/devices/device_manager.py b/roborock/devices/device_manager.py index fe497b71..cdc8b0ae 100644 --- a/roborock/devices/device_manager.py +++ b/roborock/devices/device_manager.py @@ -246,6 +246,7 @@ def device_creator(home_data: HomeData, device: HomeDataDevice, product: HomeDat channel.rpc_channel, channel.mqtt_rpc_channel, channel.map_rpc_channel, + channel.blob_rpc_channel, channel.add_dps_listener, web_api, device_cache=device_cache, diff --git a/roborock/devices/rpc/v1_channel.py b/roborock/devices/rpc/v1_channel.py index 63028e6b..8259994a 100644 --- a/roborock/devices/rpc/v1_channel.py +++ b/roborock/devices/rpc/v1_channel.py @@ -29,6 +29,7 @@ ResponseMessage, SecurityData, V1RpcChannel, + create_blob_response_decoder, create_map_response_decoder, create_security_data, decode_data_protocol_message, @@ -161,6 +162,50 @@ def find_response(response_message: RoborockMessage) -> None: return result +class BlobRpcChannel(V1RpcChannel): + """RPC channel that adds the security envelope required for blob requests.""" + + def __init__( + self, + rpc_channel: V1RpcChannel, + raw_blob_rpc_channel: V1RpcChannel, + security_data: SecurityData, + ) -> None: + """Initialize the blob RPC channel.""" + self._rpc_channel = rpc_channel + self._raw_blob_rpc_channel = raw_blob_rpc_channel + self._security_data = security_data + + async def send_command( + self, + method: CommandType, + *, + response_type: type[_T] | None = None, + params: ParamsType = None, + ) -> _T | Any: + """Send a blob command after adding the device security parameters.""" + public_key = await self._rpc_channel.send_command(RoborockCommand.GET_RANDOM_PKEY) + if not isinstance(public_key, dict) or not isinstance(public_key.get("pub_key"), dict): + raise RoborockException("get_random_pkey response did not contain a public key") + security = self._security_data.to_dict()["security"] + blob_params = { + "security": { + "pub_key": public_key["pub_key"], + "cipher_suite": 0, + }, + "endpoint": security["endpoint"], + "nonce": security["nonce"], + "data_filter": params, + } + if response_type is not None: + return await self._raw_blob_rpc_channel.send_command( + method, + response_type=response_type, + params=blob_params, + ) + return await self._raw_blob_rpc_channel.send_command(method, params=blob_params) + + class V1Channel(Channel): """Unified V1 protocol channel with automatic MQTT/local connection handling. @@ -245,6 +290,13 @@ def map_rpc_channel(self) -> V1RpcChannel: decoder = create_map_response_decoder(security_data=self._security_data) return RpcChannel(lambda: [self._create_mqtt_rpc_strategy(decoder)], self._logger) + @property + def blob_rpc_channel(self) -> V1RpcChannel: + """Return the blob RPC channel used for fetching binary content.""" + decoder = create_blob_response_decoder() + raw_blob_rpc_channel = RpcChannel(lambda: [self._create_mqtt_rpc_strategy(decoder)], self._logger) + return BlobRpcChannel(self.rpc_channel, raw_blob_rpc_channel, self._security_data) + def _create_local_rpc_strategy(self) -> RpcStrategy | None: """Create the RPC strategy for local transport.""" if self._local_channel is None or not self.is_local_connected: diff --git a/roborock/devices/traits/v1/__init__.py b/roborock/devices/traits/v1/__init__.py index 928b443e..ac8f2965 100644 --- a/roborock/devices/traits/v1/__init__.py +++ b/roborock/devices/traits/v1/__init__.py @@ -83,6 +83,7 @@ map_content, maps, network_info, + obstacle_photos, rooms, routines, smart_wash_params, @@ -105,6 +106,7 @@ from .map_content import MapContentTrait from .maps import MapsTrait from .network_info import NetworkInfoTrait +from .obstacle_photos import ObstaclePhotoTrait from .rooms import RoomsTrait from .routines import RoutinesTrait from .smart_wash_params import SmartWashParamsTrait @@ -131,6 +133,7 @@ "map_content", "maps", "network_info", + "obstacle_photos", "rooms", "routines", "smart_wash_params", @@ -171,6 +174,7 @@ class PropertiesApi(Trait): dust_collection_mode: DustCollectionModeTrait | None = None wash_towel_mode: WashTowelModeTrait | None = None smart_wash_params: SmartWashParamsTrait | None = None + obstacle_photos: ObstaclePhotoTrait | None = None def __init__( self, @@ -180,6 +184,7 @@ def __init__( rpc_channel: V1RpcChannel, mqtt_rpc_channel: V1RpcChannel, map_rpc_channel: V1RpcChannel, + blob_rpc_channel: V1RpcChannel, add_dps_listener: Callable[[Callable[[dict[RoborockDataProtocol, Any]], None]], Callable[[], None]], web_api: UserWebApiClient, device_cache: DeviceCache, @@ -191,6 +196,7 @@ def __init__( self._rpc_channel = rpc_channel self._mqtt_rpc_channel = mqtt_rpc_channel self._map_rpc_channel = map_rpc_channel + self._blob_rpc_channel = blob_rpc_channel self._web_api = web_api self._device_cache = device_cache self._region = region @@ -229,6 +235,8 @@ def _get_rpc_channel(self, trait: V1TraitMixin) -> V1RpcChannel: # to use the mqtt_rpc_channel (cloud only) instead of the rpc_channel (adaptive) if hasattr(trait, "mqtt_rpc_channel"): return self._mqtt_rpc_channel + elif hasattr(trait, "blob_rpc_channel"): + return self._blob_rpc_channel elif hasattr(trait, "map_rpc_channel"): return self._map_rpc_channel else: @@ -272,6 +280,11 @@ async def discover_features(self) -> None: wash_towel_mode._rpc_channel = self._get_rpc_channel(wash_towel_mode) # type: ignore[assignment] self.wash_towel_mode = wash_towel_mode + if self.obstacle_photos is None and self._is_supported(ObstaclePhotoTrait, "obstacle_photos", dock_features): + obstacle_photos = ObstaclePhotoTrait(self._rpc_channel) + obstacle_photos._rpc_channel = self._get_rpc_channel(obstacle_photos) + self.obstacle_photos = obstacle_photos + # Dynamically create any traits that need to be populated for item in fields(self): if (trait := getattr(self, item.name, None)) is not None: @@ -283,6 +296,8 @@ async def discover_features(self) -> None: # Union args may not be in declared order item_type = union_args[0] if union_args[1] is type(None) else union_args[1] + if item_type is ObstaclePhotoTrait: + continue if not self._is_supported(item_type, item.name, dock_features): _LOGGER.debug("Trait '%s' not supported, skipping", item.name) continue @@ -369,6 +384,7 @@ def create( rpc_channel: V1RpcChannel, mqtt_rpc_channel: V1RpcChannel, map_rpc_channel: V1RpcChannel, + blob_rpc_channel: V1RpcChannel, add_dps_listener: Callable[[Callable[[dict[RoborockDataProtocol, Any]], None]], Callable[[], None]], web_api: UserWebApiClient, device_cache: DeviceCache, @@ -383,6 +399,7 @@ def create( rpc_channel, mqtt_rpc_channel, map_rpc_channel, + blob_rpc_channel, add_dps_listener, web_api, device_cache, diff --git a/roborock/devices/traits/v1/common.py b/roborock/devices/traits/v1/common.py index 431cd075..6e7a5e3b 100644 --- a/roborock/devices/traits/v1/common.py +++ b/roborock/devices/traits/v1/common.py @@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__) -V1ResponseData = dict | list | int | str +V1ResponseData = dict | list | int | str | bytes class V1TraitDataConverter(ABC): diff --git a/roborock/devices/traits/v1/obstacle_photos.py b/roborock/devices/traits/v1/obstacle_photos.py new file mode 100644 index 00000000..3a8d9882 --- /dev/null +++ b/roborock/devices/traits/v1/obstacle_photos.py @@ -0,0 +1,104 @@ +"""Trait for fetching obstacle photos from V1 vacuums.""" + +from dataclasses import dataclass + +from roborock.data import RoborockBase +from roborock.devices.traits.v1 import common +from roborock.exceptions import RoborockException +from roborock.protocols.v1_protocol import V1RpcChannel +from roborock.roborock_typing import RoborockCommand + +_PHOTO_TYPE_SMALL = 1 +_PHOTO_DATA_BLOCK_TYPE = 3 +_MAP_OBJECT_PHOTO_ENABLED_BIT = 10 +_TYPE_SIZE = 2 +_HEADER_SIZE_SIZE = 2 +_PAYLOAD_SIZE_SIZE = 4 +_MIN_BLOCK_HEADER_SIZE = _TYPE_SIZE + _HEADER_SIZE_SIZE + _PAYLOAD_SIZE_SIZE +_IMAGE_HEADERS = (b"\x89PNG\r\n\x1a\n", b"\xff\xd8\xff") + + +@dataclass +class ObstaclePhoto(RoborockBase): + """Obstacle photo content.""" + + photo_id: str + image_content: bytes + + +class ObstaclePhotoConverter(common.V1TraitDataConverter): + """Convert a decrypted get_photo payload to an obstacle photo.""" + + def convert(self, response: common.V1ResponseData) -> ObstaclePhoto: + """Parse the response from the device into an obstacle photo.""" + if not isinstance(response, bytes): + raise ValueError(f"Unexpected ObstaclePhotoTrait response format: {type(response)}") + return ObstaclePhoto(photo_id="", image_content=parse_photo_data(response)) + + +def parse_photo_data(response: bytes) -> bytes: + """Parse the get_photo response payload and return image bytes. + + Roborock's app parses get_photo as a sequence of little-endian typed blocks. + Block type 3 contains the image bytes. + """ + offset = 0 + while offset + _MIN_BLOCK_HEADER_SIZE <= len(response): + block_type = int.from_bytes(response[offset : offset + _TYPE_SIZE], "little") + header_size = int.from_bytes( + response[offset + _TYPE_SIZE : offset + _TYPE_SIZE + _HEADER_SIZE_SIZE], + "little", + ) + payload_size = int.from_bytes( + response[offset + _TYPE_SIZE + _HEADER_SIZE_SIZE : offset + _MIN_BLOCK_HEADER_SIZE], + "little", + ) + next_offset = offset + header_size + payload_size + if header_size < _MIN_BLOCK_HEADER_SIZE or next_offset > len(response): + raise RoborockException("Invalid obstacle photo payload") + + if block_type == _PHOTO_DATA_BLOCK_TYPE: + image_content = response[offset + header_size : next_offset] + if not image_content.startswith(_IMAGE_HEADERS): + raise RoborockException("Obstacle photo payload is not a supported image") + return image_content + + offset = next_offset + + raise RoborockException("Obstacle photo payload does not contain photo data") + + +class ObstaclePhotoTrait(RoborockBase, common.V1TraitMixin): + """Trait for fetching obstacle photos.""" + + command = RoborockCommand.GET_PHOTO + converter = ObstaclePhotoConverter() + blob_rpc_channel = True + requires_feature = "is_ai_recognition_obstacle_supported" + + def __init__(self, standard_rpc_channel: V1RpcChannel) -> None: + """Initialize the obstacle photo trait.""" + super().__init__() + self._standard_rpc_channel = standard_rpc_channel + + async def get_enabled(self) -> bool: + """Return whether map object photo capture is enabled on the vacuum.""" + response = await self._standard_rpc_channel.send_command(RoborockCommand.GET_CAMERA_STATUS) + if not isinstance(response, list) or not response or not isinstance(response[0], int): + raise RoborockException("get_camera_status response did not contain camera status") + return bool((response[0] >> _MAP_OBJECT_PHOTO_ENABLED_BIT) & 1) + + async def get_photo(self, photo_id: str) -> ObstaclePhoto: + """Fetch the small obstacle photo for a map photo ID. + + Photo IDs are available as ``Obstacle.details.photo_name`` in + ``ParsedMapData.map_data.obstacles_with_photo`` and + ``ParsedMapData.map_data.ignored_obstacles_with_photo``. + """ + response = await self.rpc_channel.send_command( + self.command, + params={"img_id": photo_id, "type": _PHOTO_TYPE_SMALL}, + ) + photo = self.converter.convert(response) + photo.photo_id = photo_id + return photo diff --git a/roborock/protocols/v1_protocol.py b/roborock/protocols/v1_protocol.py index 4b01501d..733f48a1 100644 --- a/roborock/protocols/v1_protocol.py +++ b/roborock/protocols/v1_protocol.py @@ -3,10 +3,12 @@ from __future__ import annotations import base64 +import gzip import json import logging import secrets import struct +import zlib from collections.abc import Callable from dataclasses import dataclass, field from enum import StrEnum @@ -24,6 +26,7 @@ __all__ = [ "SecurityData", "create_security_data", + "create_blob_response_decoder", "decode_data_protocol_message", "decode_rpc_response", "V1RpcChannel", @@ -261,6 +264,48 @@ class MapResponse: """The map data, decrypted and decompressed.""" +@dataclass +class BlobResponse: + """Data structure for V1 blob responses.""" + + request_id: int + """The request ID of the blob response.""" + + data: bytes + """The blob data, decompressed.""" + + +def create_blob_response_decoder() -> Callable[[RoborockMessage], BlobResponse | None]: + """Create a decoder for V1 blob response messages. + + Obstacle photos are acknowledged through the normal RPC response with + ``["ok"]`` and delivered later as a protocol-301 blob frame. The frame starts + with ``ROBOROCK``, stores the RPC request id at bytes 8-11, the header size + at bytes 16-17, and the gzip payload length at bytes 20-23. + """ + + def _decode_blob_response(message: RoborockMessage) -> BlobResponse | None: + """Decode a V1 blob response message.""" + if message.protocol != RoborockMessageProtocol.MAP_RESPONSE: + return None + payload = message.payload + if not payload or not payload.startswith(b"ROBOROCK") or len(payload) < 24: + return None + request_id = int.from_bytes(payload[8:12], "little") + header_size = int.from_bytes(payload[16:18], "little") + payload_size = int.from_bytes(payload[20:24], "little") + end_offset = header_size + payload_size + if header_size < 24 or end_offset > len(payload): + raise RoborockException("Invalid V1 blob response format") + try: + data = Utils.decompress(payload[header_size:end_offset]) + except (gzip.BadGzipFile, EOFError, zlib.error) as err: + raise RoborockException("Failed to decode blob message payload") from err + return BlobResponse(request_id=request_id, data=data) + + return _decode_blob_response + + def create_map_response_decoder(security_data: SecurityData) -> Callable[[RoborockMessage], MapResponse | None]: """Create a decoder for V1 map response messages.""" diff --git a/roborock/roborock_typing.py b/roborock/roborock_typing.py index 71251324..e6fd5043 100644 --- a/roborock/roborock_typing.py +++ b/roborock/roborock_typing.py @@ -130,6 +130,7 @@ class RoborockCommand(str, Enum): GET_NETWORK_INFO = "get_network_info" GET_OFFLINE_MAP_STATUS = "get_offline_map_status" GET_PERSIST = "get_persist_map" + GET_PHOTO = "get_photo" GET_PROP = "get_prop" GET_RANDOM_PKEY = "get_random_pkey" GET_RECOVER_MAP = "get_recover_map" diff --git a/tests/__snapshots__/test_supported_features.ambr b/tests/__snapshots__/test_supported_features.ambr index 196190d5..447a6822 100644 --- a/tests/__snapshots__/test_supported_features.ambr +++ b/tests/__snapshots__/test_supported_features.ambr @@ -2,6 +2,8 @@ # name: test_device_features_from_home_data[home_data_device_pearls.json] dict({ 'is_activate_video_charging_and_standby_supported': False, + 'is_ai_recognition_obstacle_supported': True, + 'is_ai_recognition_setting_supported': True, 'is_analysis_supported': False, 'is_any_state_transit_goto_supported': True, 'is_auto_collection_2_supported': True, @@ -178,6 +180,8 @@ # name: test_device_features_from_home_data[home_data_device_q10.json] dict({ 'is_activate_video_charging_and_standby_supported': False, + 'is_ai_recognition_obstacle_supported': True, + 'is_ai_recognition_setting_supported': True, 'is_analysis_supported': False, 'is_any_state_transit_goto_supported': False, 'is_auto_collection_2_supported': False, @@ -354,6 +358,8 @@ # name: test_device_features_from_home_data[home_data_device_q5_max.json] dict({ 'is_activate_video_charging_and_standby_supported': False, + 'is_ai_recognition_obstacle_supported': False, + 'is_ai_recognition_setting_supported': False, 'is_analysis_supported': False, 'is_any_state_transit_goto_supported': True, 'is_auto_collection_2_supported': False, @@ -530,6 +536,8 @@ # name: test_device_features_from_home_data[home_data_device_q7.json] dict({ 'is_activate_video_charging_and_standby_supported': False, + 'is_ai_recognition_obstacle_supported': True, + 'is_ai_recognition_setting_supported': True, 'is_analysis_supported': False, 'is_any_state_transit_goto_supported': False, 'is_auto_collection_2_supported': False, @@ -706,6 +714,8 @@ # name: test_device_features_from_home_data[home_data_device_s5e.json] dict({ 'is_activate_video_charging_and_standby_supported': False, + 'is_ai_recognition_obstacle_supported': False, + 'is_ai_recognition_setting_supported': False, 'is_analysis_supported': False, 'is_any_state_transit_goto_supported': False, 'is_auto_collection_2_supported': False, @@ -882,6 +892,8 @@ # name: test_device_features_from_home_data[home_data_device_s7_maxv.json] dict({ 'is_activate_video_charging_and_standby_supported': False, + 'is_ai_recognition_obstacle_supported': True, + 'is_ai_recognition_setting_supported': True, 'is_analysis_supported': False, 'is_any_state_transit_goto_supported': True, 'is_auto_collection_2_supported': False, @@ -1058,6 +1070,8 @@ # name: test_device_features_from_home_data[home_data_device_saros.json] dict({ 'is_activate_video_charging_and_standby_supported': False, + 'is_ai_recognition_obstacle_supported': True, + 'is_ai_recognition_setting_supported': True, 'is_analysis_supported': False, 'is_any_state_transit_goto_supported': True, 'is_auto_collection_2_supported': True, @@ -1234,6 +1248,8 @@ # name: test_device_features_from_home_data[home_data_device_saros_10r.json] dict({ 'is_activate_video_charging_and_standby_supported': True, + 'is_ai_recognition_obstacle_supported': True, + 'is_ai_recognition_setting_supported': True, 'is_analysis_supported': False, 'is_any_state_transit_goto_supported': True, 'is_auto_collection_2_supported': True, @@ -1410,6 +1426,8 @@ # name: test_device_features_from_home_data[home_data_device_zeo_one.json] dict({ 'is_activate_video_charging_and_standby_supported': False, + 'is_ai_recognition_obstacle_supported': True, + 'is_ai_recognition_setting_supported': True, 'is_analysis_supported': False, 'is_any_state_transit_goto_supported': False, 'is_auto_collection_2_supported': False, diff --git a/tests/devices/__snapshots__/test_device_manager.ambr b/tests/devices/__snapshots__/test_device_manager.ambr index 558e04e8..43a91d1a 100644 --- a/tests/devices/__snapshots__/test_device_manager.ambr +++ b/tests/devices/__snapshots__/test_device_manager.ambr @@ -184,6 +184,8 @@ 125, ]), 'isActivateVideoChargingAndStandbySupported': False, + 'isAiRecognitionObstacleSupported': True, + 'isAiRecognitionSettingSupported': True, 'isAnalysisSupported': True, 'isAnyStateTransitGotoSupported': False, 'isAutoCollection2Supported': False, diff --git a/tests/devices/__snapshots__/test_file_cache.ambr b/tests/devices/__snapshots__/test_file_cache.ambr index 0a634d5d..85f15dd6 100644 --- a/tests/devices/__snapshots__/test_file_cache.ambr +++ b/tests/devices/__snapshots__/test_file_cache.ambr @@ -1,6 +1,6 @@ # serializer version: 1 # name: test_set_and_flush_and_get[default-all_fields_cache] - CacheData(home_data=HomeData(id=123456, name='My Home', products=[HomeDataProduct(id='product-id-s7-maxv', name='Roborock S7 MaxV', model='roborock.vacuum.a27', category=, code='a27', icon_url=None, attribute=None, capability=0, schema=[HomeDataProductSchema(id='101', name='rpc_request', code='rpc_request_code', mode='rw', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='102', name='rpc_response', code='rpc_response', mode='rw', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='120', name='错误代码', code='error_code', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='121', name='设备状态', code='state', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='122', name='设备电量', code='battery', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='123', name='清扫模式', code='fan_power', mode='rw', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='124', name='拖地模式', code='water_box_mode', mode='rw', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='125', name='主刷寿命', code='main_brush_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='126', name='边刷寿命', code='side_brush_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='127', name='滤网寿命', code='filter_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='128', name='额外状态', code='additional_props', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='130', name='完成事件', code='task_complete', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='131', name='电量不足任务取消', code='task_cancel_low_power', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='132', name='运动中任务取消', code='task_cancel_in_motion', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='133', name='充电状态', code='charge_status', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='134', name='烘干状态', code='drying_status', mode='ro', type='RAW', product_property=None, property=None, desc=None)])], devices=[HomeDataDevice(duid='abc123', name='Roborock S7 MaxV', local_key='key123key123key1', product_id='product-id-s7-maxv', fv='02.56.02', attribute=None, active_time=1672364449, runtime_env=None, time_zone_id='America/Los_Angeles', icon_url='no_url', lon=None, lat=None, share=False, share_time=None, online=True, pv='1.0', room_id=2362003, tuya_uuid=None, tuya_migrated=False, extra='{"RRPhotoPrivacyVersion": "1"}', sn='abc123', feature_set='2234201184108543', new_feature_set='0000000000002041', device_status={'121': 8, '122': 100, '123': 102, '124': 203, '125': 94, '126': 90, '127': 87, '128': 0, '133': 1, '120': 0}, silent_ota_switch=True, setting=None, f=None, create_time=None, cid=None, share_type=None, share_expired_time=None)], received_devices=[], lon=None, lat=None, geo_name=None, rooms=[HomeDataRoom(id=2362048, name='Example room 1'), HomeDataRoom(id=2362044, name='Example room 2'), HomeDataRoom(id=2362041, name='Example room 3')]), device_info={'device1': DeviceCacheData(network_info=NetworkInfo(ip='1.1.1.1', ssid='test_wifi', mac='aa:bb:cc:dd:ee:ff', bssid='aa:bb:cc:dd:ee:ff', rssi=-50), home_map_info={1: CombinedMapInfo(map_flag=1, name='Test Map 1', rooms=[NamedRoomMapping(segment_id=1023, iot_id='4321', raw_name='Living Room'), NamedRoomMapping(segment_id=1024, iot_id='4322', raw_name='Starship')]), 2: CombinedMapInfo(map_flag=2, name='Test Map 2', rooms=[NamedRoomMapping(segment_id=2047, iot_id='5432', raw_name='Bedroom')])}, home_map_content_base64={1: 'ZHVtbXlfbWFwX2NvbnRlbnQ=', 2: 'bW9yZV9kdW1teV9jb250ZW50'}, device_features=DeviceFeatures(is_show_clean_finish_reason_supported=True, is_re_segment_supported=False, is_video_monitor_supported=False, is_any_state_transit_goto_supported=False, is_fw_filter_obstacle_supported=False, is_video_setting_supported=False, is_ignore_unknown_map_object_supported=False, is_set_child_supported=False, is_carpet_supported=False, is_record_allowed=False, is_mop_path_supported=False, is_multi_map_segment_timer_supported=False, is_current_map_restore_enabled=False, is_room_name_supported=False, is_shake_mop_set_supported=False, is_map_beautify_internal_debug_supported=False, is_new_data_for_clean_history=False, is_new_data_for_clean_history_detail=False, is_flow_led_setting_supported=False, is_dust_collection_setting_supported=False, is_rpc_retry_supported=False, is_avoid_collision_supported=False, is_support_set_switch_map_mode=False, is_map_carpet_add_support=False, is_custom_water_box_distance_supported=False, is_support_smart_scene=False, is_support_floor_edit=False, is_support_furniture=False, is_wash_then_charge_cmd_supported=False, is_support_room_tag=False, is_support_quick_map_builder=False, is_support_smart_global_clean_with_custom_mode=False, is_careful_slow_mop_supported=False, is_egg_mode_supported_from_new_features=False, is_carpet_show_on_map=False, is_supported_valley_electricity=False, is_unsave_map_reason_supported=False, is_supported_drying=False, is_supported_download_test_voice=False, is_support_backup_map=False, is_support_custom_mode_in_cleaning=False, is_support_remote_control_in_call=False, is_support_set_volume_in_call=True, is_support_clean_estimate=False, is_support_custom_dnd=False, is_carpet_deep_clean_supported=False, is_support_stuck_zone=False, is_support_custom_door_sill=False, is_wifi_manage_supported=False, is_clean_route_fast_mode_supported=False, is_support_cliff_zone=False, is_support_smart_door_sill=False, is_support_floor_direction=False, is_back_charge_auto_wash_supported=False, is_support_incremental_map=False, is_offline_map_supported=False, is_super_deep_wash_supported=False, is_ces_2022_supported=False, is_dss_believable=False, is_main_brush_up_down_supported_from_str=False, is_goto_pure_clean_path_supported=False, is_water_up_down_drain_supported=False, is_setting_carpet_first_supported=False, is_clean_route_deep_slow_plus_supported=False, is_dynamically_skip_clean_zone_supported=False, is_dynamically_add_clean_zones_supported=False, is_left_water_drain_supported=False, is_clean_count_setting_supported=False, is_corner_clean_mode_supported=False, is_two_key_real_time_video_supported=False, is_two_key_rtv_in_charging_supported=False, is_dirty_replenish_clean_supported=False, is_auto_delivery_field_in_global_status_supported=False, is_avoid_collision_mode_supported=False, is_voice_control_supported=False, is_new_endpoint_supported=False, is_pumping_water_supported=False, is_corner_mop_stretch_supported=False, is_hot_wash_towel_supported=False, is_floor_dir_clean_any_time_supported=False, is_pet_supplies_deep_clean_supported=False, is_mop_shake_water_max_supported=False, is_exact_custom_mode_supported=False, is_video_patrol_supported=False, is_carpet_custom_clean_supported=False, is_pet_snapshot_supported=False, is_custom_clean_mode_count_supported=False, is_new_ai_recognition_supported=False, is_auto_collection_2_supported=False, is_right_brush_stretch_supported=False, is_smart_clean_mode_set_supported=False, is_dirty_object_detect_supported=False, is_no_need_carpet_press_set_supported=False, is_voice_control_led_supported=False, is_water_leak_check_supported=False, is_min_battery_15_to_clean_task_supported=False, is_gap_deep_clean_supported=False, is_object_detect_check_supported=False, is_identify_room_supported=False, is_matter_supported=False, is_workday_holiday_supported=False, is_clean_direct_status_supported=False, is_map_eraser_supported=False, is_optimize_battery_supported=False, is_activate_video_charging_and_standby_supported=False, is_carpet_long_haired_supported=False, is_clean_history_time_line_supported=False, is_max_zone_opened_supported=False, is_exhibition_function_supported=False, is_lds_lifting_supported=False, is_auto_tear_down_mop_supported=False, is_small_side_mop_supported=False, is_support_side_brush_up_down_supported=False, is_dry_interval_timer_supported=False, is_uvc_sterilize_supported=False, is_midway_back_to_dock_supported=False, is_support_main_brush_up_down_supported=False, is_egg_dance_mode_supported=False, is_mechanical_arm_mode_supported=False, is_tidyup_zones_supported=False, is_clean_time_line_supported=False, is_clean_then_mop_mode_supported=False, is_type_identify_supported=False, is_support_get_particular_status_supported=False, is_three_d_mapping_inner_test_supported=False, is_sync_server_name_supported=False, is_should_show_arm_over_load_supported=False, is_collect_dust_count_show_supported=False, is_support_api_app_stop_grasp_supported=False, is_ctm_with_repeat_supported=False, is_side_brush_lift_carpet_supported=False, is_detect_wire_carpet_supported=False, is_water_slide_mode_supported=False, is_soak_and_wash_supported=False, is_clean_efficiency_supported=False, is_back_wash_new_smart_supported=False, is_dual_band_wi_fi_supported=False, is_program_mode_supported=False, is_clean_fluid_delivery_supported=False, is_carpet_long_haired_ex_supported=False, is_over_sea_ctm_supported=False, is_full_duples_switch_supported=False, is_low_area_access_supported=False, is_follow_low_obs_supported=False, is_two_gears_no_collision_supported=False, is_carpet_shape_type_supported=False, is_sr_map_supported=False, is_led_status_switch_supported=False, is_multi_floor_supported=False, is_support_fetch_timer_summary=False, is_order_clean_supported=False, is_analysis_supported=False, is_remote_supported=False, is_support_voice_control_debug=False, is_mop_forbidden_supported=True, is_soft_clean_mode_supported=True, is_custom_mode_supported=True, is_support_custom_carpet=True, is_show_general_obstacle_supported=True, is_show_obstacle_photo_supported=True, is_rubber_brush_carpet_supported=True, is_carpet_pressure_use_origin_paras_supported=True, is_support_mop_back_pwm_set=True, is_collect_dust_mode_supported=True, is_support_water_mode=False, is_pure_clean_mop_supported=False, is_new_remote_view_supported=False, is_max_plus_mode_supported=False, is_none_pure_clean_mop_with_max_plus=False, is_clean_route_setting_supported=False, is_mop_shake_module_supported=False, is_customized_clean_supported=False), trait_data={'test_trait': {'key': 'value', 'number': 42}}), 'device2': DeviceCacheData(network_info=None, home_map_info=None, home_map_content_base64=None, device_features=None, trait_data=None)}, network_info={}, home_map_info={}, home_map_content={}, home_map_content_base64={}, device_features=None, trait_data=None) + CacheData(home_data=HomeData(id=123456, name='My Home', products=[HomeDataProduct(id='product-id-s7-maxv', name='Roborock S7 MaxV', model='roborock.vacuum.a27', category=, code='a27', icon_url=None, attribute=None, capability=0, schema=[HomeDataProductSchema(id='101', name='rpc_request', code='rpc_request_code', mode='rw', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='102', name='rpc_response', code='rpc_response', mode='rw', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='120', name='错误代码', code='error_code', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='121', name='设备状态', code='state', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='122', name='设备电量', code='battery', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='123', name='清扫模式', code='fan_power', mode='rw', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='124', name='拖地模式', code='water_box_mode', mode='rw', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='125', name='主刷寿命', code='main_brush_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='126', name='边刷寿命', code='side_brush_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='127', name='滤网寿命', code='filter_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='128', name='额外状态', code='additional_props', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='130', name='完成事件', code='task_complete', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='131', name='电量不足任务取消', code='task_cancel_low_power', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='132', name='运动中任务取消', code='task_cancel_in_motion', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='133', name='充电状态', code='charge_status', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='134', name='烘干状态', code='drying_status', mode='ro', type='RAW', product_property=None, property=None, desc=None)])], devices=[HomeDataDevice(duid='abc123', name='Roborock S7 MaxV', local_key='key123key123key1', product_id='product-id-s7-maxv', fv='02.56.02', attribute=None, active_time=1672364449, runtime_env=None, time_zone_id='America/Los_Angeles', icon_url='no_url', lon=None, lat=None, share=False, share_time=None, online=True, pv='1.0', room_id=2362003, tuya_uuid=None, tuya_migrated=False, extra='{"RRPhotoPrivacyVersion": "1"}', sn='abc123', feature_set='2234201184108543', new_feature_set='0000000000002041', device_status={'121': 8, '122': 100, '123': 102, '124': 203, '125': 94, '126': 90, '127': 87, '128': 0, '133': 1, '120': 0}, silent_ota_switch=True, setting=None, f=None, create_time=None, cid=None, share_type=None, share_expired_time=None)], received_devices=[], lon=None, lat=None, geo_name=None, rooms=[HomeDataRoom(id=2362048, name='Example room 1'), HomeDataRoom(id=2362044, name='Example room 2'), HomeDataRoom(id=2362041, name='Example room 3')]), device_info={'device1': DeviceCacheData(network_info=NetworkInfo(ip='1.1.1.1', ssid='test_wifi', mac='aa:bb:cc:dd:ee:ff', bssid='aa:bb:cc:dd:ee:ff', rssi=-50), home_map_info={1: CombinedMapInfo(map_flag=1, name='Test Map 1', rooms=[NamedRoomMapping(segment_id=1023, iot_id='4321', raw_name='Living Room'), NamedRoomMapping(segment_id=1024, iot_id='4322', raw_name='Starship')]), 2: CombinedMapInfo(map_flag=2, name='Test Map 2', rooms=[NamedRoomMapping(segment_id=2047, iot_id='5432', raw_name='Bedroom')])}, home_map_content_base64={1: 'ZHVtbXlfbWFwX2NvbnRlbnQ=', 2: 'bW9yZV9kdW1teV9jb250ZW50'}, device_features=DeviceFeatures(is_show_clean_finish_reason_supported=True, is_re_segment_supported=False, is_video_monitor_supported=False, is_any_state_transit_goto_supported=False, is_fw_filter_obstacle_supported=False, is_video_setting_supported=False, is_ignore_unknown_map_object_supported=False, is_set_child_supported=False, is_carpet_supported=False, is_record_allowed=False, is_mop_path_supported=False, is_multi_map_segment_timer_supported=False, is_current_map_restore_enabled=False, is_room_name_supported=False, is_shake_mop_set_supported=False, is_map_beautify_internal_debug_supported=False, is_new_data_for_clean_history=False, is_new_data_for_clean_history_detail=False, is_flow_led_setting_supported=False, is_dust_collection_setting_supported=False, is_rpc_retry_supported=False, is_avoid_collision_supported=False, is_support_set_switch_map_mode=False, is_map_carpet_add_support=False, is_custom_water_box_distance_supported=False, is_support_smart_scene=False, is_support_floor_edit=False, is_support_furniture=False, is_wash_then_charge_cmd_supported=False, is_support_room_tag=False, is_support_quick_map_builder=False, is_support_smart_global_clean_with_custom_mode=False, is_careful_slow_mop_supported=False, is_egg_mode_supported_from_new_features=False, is_carpet_show_on_map=False, is_supported_valley_electricity=False, is_unsave_map_reason_supported=False, is_supported_drying=False, is_supported_download_test_voice=False, is_support_backup_map=False, is_support_custom_mode_in_cleaning=False, is_support_remote_control_in_call=False, is_support_set_volume_in_call=True, is_support_clean_estimate=False, is_support_custom_dnd=False, is_carpet_deep_clean_supported=False, is_support_stuck_zone=False, is_support_custom_door_sill=False, is_wifi_manage_supported=False, is_clean_route_fast_mode_supported=False, is_support_cliff_zone=False, is_support_smart_door_sill=False, is_support_floor_direction=False, is_back_charge_auto_wash_supported=False, is_support_incremental_map=False, is_offline_map_supported=False, is_super_deep_wash_supported=False, is_ces_2022_supported=False, is_dss_believable=False, is_main_brush_up_down_supported_from_str=False, is_goto_pure_clean_path_supported=False, is_water_up_down_drain_supported=False, is_setting_carpet_first_supported=False, is_clean_route_deep_slow_plus_supported=False, is_dynamically_skip_clean_zone_supported=False, is_dynamically_add_clean_zones_supported=False, is_left_water_drain_supported=False, is_clean_count_setting_supported=False, is_corner_clean_mode_supported=False, is_two_key_real_time_video_supported=False, is_two_key_rtv_in_charging_supported=False, is_dirty_replenish_clean_supported=False, is_auto_delivery_field_in_global_status_supported=False, is_avoid_collision_mode_supported=False, is_voice_control_supported=False, is_new_endpoint_supported=False, is_pumping_water_supported=False, is_corner_mop_stretch_supported=False, is_hot_wash_towel_supported=False, is_floor_dir_clean_any_time_supported=False, is_pet_supplies_deep_clean_supported=False, is_mop_shake_water_max_supported=False, is_exact_custom_mode_supported=False, is_video_patrol_supported=False, is_carpet_custom_clean_supported=False, is_pet_snapshot_supported=False, is_custom_clean_mode_count_supported=False, is_new_ai_recognition_supported=False, is_auto_collection_2_supported=False, is_right_brush_stretch_supported=False, is_smart_clean_mode_set_supported=False, is_dirty_object_detect_supported=False, is_no_need_carpet_press_set_supported=False, is_voice_control_led_supported=False, is_water_leak_check_supported=False, is_min_battery_15_to_clean_task_supported=False, is_gap_deep_clean_supported=False, is_object_detect_check_supported=False, is_identify_room_supported=False, is_matter_supported=False, is_workday_holiday_supported=False, is_clean_direct_status_supported=False, is_map_eraser_supported=False, is_optimize_battery_supported=False, is_activate_video_charging_and_standby_supported=False, is_carpet_long_haired_supported=False, is_clean_history_time_line_supported=False, is_max_zone_opened_supported=False, is_exhibition_function_supported=False, is_lds_lifting_supported=False, is_auto_tear_down_mop_supported=False, is_small_side_mop_supported=False, is_support_side_brush_up_down_supported=False, is_dry_interval_timer_supported=False, is_uvc_sterilize_supported=False, is_midway_back_to_dock_supported=False, is_support_main_brush_up_down_supported=False, is_egg_dance_mode_supported=False, is_mechanical_arm_mode_supported=False, is_tidyup_zones_supported=False, is_clean_time_line_supported=False, is_clean_then_mop_mode_supported=False, is_type_identify_supported=False, is_support_get_particular_status_supported=False, is_three_d_mapping_inner_test_supported=False, is_sync_server_name_supported=False, is_should_show_arm_over_load_supported=False, is_collect_dust_count_show_supported=False, is_support_api_app_stop_grasp_supported=False, is_ctm_with_repeat_supported=False, is_side_brush_lift_carpet_supported=False, is_detect_wire_carpet_supported=False, is_water_slide_mode_supported=False, is_soak_and_wash_supported=False, is_clean_efficiency_supported=False, is_back_wash_new_smart_supported=False, is_dual_band_wi_fi_supported=False, is_program_mode_supported=False, is_clean_fluid_delivery_supported=False, is_carpet_long_haired_ex_supported=False, is_over_sea_ctm_supported=False, is_full_duples_switch_supported=False, is_low_area_access_supported=False, is_follow_low_obs_supported=False, is_two_gears_no_collision_supported=False, is_carpet_shape_type_supported=False, is_sr_map_supported=False, is_led_status_switch_supported=False, is_multi_floor_supported=False, is_support_fetch_timer_summary=False, is_order_clean_supported=False, is_analysis_supported=False, is_remote_supported=False, is_support_voice_control_debug=False, is_mop_forbidden_supported=True, is_soft_clean_mode_supported=True, is_custom_mode_supported=True, is_support_custom_carpet=True, is_show_general_obstacle_supported=True, is_show_obstacle_photo_supported=True, is_rubber_brush_carpet_supported=True, is_carpet_pressure_use_origin_paras_supported=True, is_support_mop_back_pwm_set=True, is_collect_dust_mode_supported=True, is_support_water_mode=False, is_ai_recognition_setting_supported=False, is_ai_recognition_obstacle_supported=False, is_pure_clean_mop_supported=False, is_new_remote_view_supported=False, is_max_plus_mode_supported=False, is_none_pure_clean_mop_with_max_plus=False, is_clean_route_setting_supported=False, is_mop_shake_module_supported=False, is_customized_clean_supported=False), trait_data={'test_trait': {'key': 'value', 'number': 42}}), 'device2': DeviceCacheData(network_info=None, home_map_info=None, home_map_content_base64=None, device_features=None, trait_data=None)}, network_info={}, home_map_info={}, home_map_content={}, home_map_content_base64={}, device_features=None, trait_data=None) # --- # name: test_set_and_flush_and_get[default-empty_cache] CacheData(home_data=None, device_info={}, network_info={}, home_map_info={}, home_map_content={}, home_map_content_base64={}, device_features=None, trait_data=None) @@ -12,7 +12,7 @@ CacheData(home_data=HomeData(id=123456, name='My Home', products=[HomeDataProduct(id='product-id-s7-maxv', name='Roborock S7 MaxV', model='roborock.vacuum.a27', category=, code='a27', icon_url=None, attribute=None, capability=0, schema=[HomeDataProductSchema(id='101', name='rpc_request', code='rpc_request_code', mode='rw', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='102', name='rpc_response', code='rpc_response', mode='rw', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='120', name='错误代码', code='error_code', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='121', name='设备状态', code='state', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='122', name='设备电量', code='battery', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='123', name='清扫模式', code='fan_power', mode='rw', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='124', name='拖地模式', code='water_box_mode', mode='rw', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='125', name='主刷寿命', code='main_brush_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='126', name='边刷寿命', code='side_brush_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='127', name='滤网寿命', code='filter_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='128', name='额外状态', code='additional_props', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='130', name='完成事件', code='task_complete', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='131', name='电量不足任务取消', code='task_cancel_low_power', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='132', name='运动中任务取消', code='task_cancel_in_motion', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='133', name='充电状态', code='charge_status', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='134', name='烘干状态', code='drying_status', mode='ro', type='RAW', product_property=None, property=None, desc=None)])], devices=[HomeDataDevice(duid='abc123', name='Roborock S7 MaxV', local_key='key123key123key1', product_id='product-id-s7-maxv', fv='02.56.02', attribute=None, active_time=1672364449, runtime_env=None, time_zone_id='America/Los_Angeles', icon_url='no_url', lon=None, lat=None, share=False, share_time=None, online=True, pv='1.0', room_id=2362003, tuya_uuid=None, tuya_migrated=False, extra='{"RRPhotoPrivacyVersion": "1"}', sn='abc123', feature_set='2234201184108543', new_feature_set='0000000000002041', device_status={'121': 8, '122': 100, '123': 102, '124': 203, '125': 94, '126': 90, '127': 87, '128': 0, '133': 1, '120': 0}, silent_ota_switch=True, setting=None, f=None, create_time=None, cid=None, share_type=None, share_expired_time=None)], received_devices=[], lon=None, lat=None, geo_name=None, rooms=[HomeDataRoom(id=2362048, name='Example room 1'), HomeDataRoom(id=2362044, name='Example room 2'), HomeDataRoom(id=2362041, name='Example room 3')]), device_info={}, network_info={}, home_map_info={}, home_map_content={}, home_map_content_base64={}, device_features=None, trait_data=None) # --- # name: test_set_and_flush_and_get[json-all_fields_cache] - CacheData(home_data=HomeData(id=123456, name='My Home', products=[HomeDataProduct(id='product-id-s7-maxv', name='Roborock S7 MaxV', model='roborock.vacuum.a27', category=, code='a27', icon_url=None, attribute=None, capability=0, schema=[HomeDataProductSchema(id='101', name='rpc_request', code='rpc_request_code', mode='rw', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='102', name='rpc_response', code='rpc_response', mode='rw', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='120', name='错误代码', code='error_code', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='121', name='设备状态', code='state', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='122', name='设备电量', code='battery', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='123', name='清扫模式', code='fan_power', mode='rw', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='124', name='拖地模式', code='water_box_mode', mode='rw', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='125', name='主刷寿命', code='main_brush_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='126', name='边刷寿命', code='side_brush_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='127', name='滤网寿命', code='filter_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='128', name='额外状态', code='additional_props', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='130', name='完成事件', code='task_complete', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='131', name='电量不足任务取消', code='task_cancel_low_power', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='132', name='运动中任务取消', code='task_cancel_in_motion', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='133', name='充电状态', code='charge_status', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='134', name='烘干状态', code='drying_status', mode='ro', type='RAW', product_property=None, property=None, desc=None)])], devices=[HomeDataDevice(duid='abc123', name='Roborock S7 MaxV', local_key='key123key123key1', product_id='product-id-s7-maxv', fv='02.56.02', attribute=None, active_time=1672364449, runtime_env=None, time_zone_id='America/Los_Angeles', icon_url='no_url', lon=None, lat=None, share=False, share_time=None, online=True, pv='1.0', room_id=2362003, tuya_uuid=None, tuya_migrated=False, extra='{"RRPhotoPrivacyVersion": "1"}', sn='abc123', feature_set='2234201184108543', new_feature_set='0000000000002041', device_status={'121': 8, '122': 100, '123': 102, '124': 203, '125': 94, '126': 90, '127': 87, '128': 0, '133': 1, '120': 0}, silent_ota_switch=True, setting=None, f=None, create_time=None, cid=None, share_type=None, share_expired_time=None)], received_devices=[], lon=None, lat=None, geo_name=None, rooms=[HomeDataRoom(id=2362048, name='Example room 1'), HomeDataRoom(id=2362044, name='Example room 2'), HomeDataRoom(id=2362041, name='Example room 3')]), device_info={'device1': DeviceCacheData(network_info=NetworkInfo(ip='1.1.1.1', ssid='test_wifi', mac='aa:bb:cc:dd:ee:ff', bssid='aa:bb:cc:dd:ee:ff', rssi=-50), home_map_info={1: CombinedMapInfo(map_flag=1, name='Test Map 1', rooms=[NamedRoomMapping(segment_id=1023, iot_id='4321', raw_name='Living Room'), NamedRoomMapping(segment_id=1024, iot_id='4322', raw_name='Starship')]), 2: CombinedMapInfo(map_flag=2, name='Test Map 2', rooms=[NamedRoomMapping(segment_id=2047, iot_id='5432', raw_name='Bedroom')])}, home_map_content_base64={1: 'ZHVtbXlfbWFwX2NvbnRlbnQ=', 2: 'bW9yZV9kdW1teV9jb250ZW50'}, device_features=DeviceFeatures(is_show_clean_finish_reason_supported=True, is_re_segment_supported=False, is_video_monitor_supported=False, is_any_state_transit_goto_supported=False, is_fw_filter_obstacle_supported=False, is_video_setting_supported=False, is_ignore_unknown_map_object_supported=False, is_set_child_supported=False, is_carpet_supported=False, is_record_allowed=False, is_mop_path_supported=False, is_multi_map_segment_timer_supported=False, is_current_map_restore_enabled=False, is_room_name_supported=False, is_shake_mop_set_supported=False, is_map_beautify_internal_debug_supported=False, is_new_data_for_clean_history=False, is_new_data_for_clean_history_detail=False, is_flow_led_setting_supported=False, is_dust_collection_setting_supported=False, is_rpc_retry_supported=False, is_avoid_collision_supported=False, is_support_set_switch_map_mode=False, is_map_carpet_add_support=False, is_custom_water_box_distance_supported=False, is_support_smart_scene=False, is_support_floor_edit=False, is_support_furniture=False, is_wash_then_charge_cmd_supported=False, is_support_room_tag=False, is_support_quick_map_builder=False, is_support_smart_global_clean_with_custom_mode=False, is_careful_slow_mop_supported=False, is_egg_mode_supported_from_new_features=False, is_carpet_show_on_map=False, is_supported_valley_electricity=False, is_unsave_map_reason_supported=False, is_supported_drying=False, is_supported_download_test_voice=False, is_support_backup_map=False, is_support_custom_mode_in_cleaning=False, is_support_remote_control_in_call=False, is_support_set_volume_in_call=True, is_support_clean_estimate=False, is_support_custom_dnd=False, is_carpet_deep_clean_supported=False, is_support_stuck_zone=False, is_support_custom_door_sill=False, is_wifi_manage_supported=False, is_clean_route_fast_mode_supported=False, is_support_cliff_zone=False, is_support_smart_door_sill=False, is_support_floor_direction=False, is_back_charge_auto_wash_supported=False, is_support_incremental_map=False, is_offline_map_supported=False, is_super_deep_wash_supported=False, is_ces_2022_supported=False, is_dss_believable=False, is_main_brush_up_down_supported_from_str=False, is_goto_pure_clean_path_supported=False, is_water_up_down_drain_supported=False, is_setting_carpet_first_supported=False, is_clean_route_deep_slow_plus_supported=False, is_dynamically_skip_clean_zone_supported=False, is_dynamically_add_clean_zones_supported=False, is_left_water_drain_supported=False, is_clean_count_setting_supported=False, is_corner_clean_mode_supported=False, is_two_key_real_time_video_supported=False, is_two_key_rtv_in_charging_supported=False, is_dirty_replenish_clean_supported=False, is_auto_delivery_field_in_global_status_supported=False, is_avoid_collision_mode_supported=False, is_voice_control_supported=False, is_new_endpoint_supported=False, is_pumping_water_supported=False, is_corner_mop_stretch_supported=False, is_hot_wash_towel_supported=False, is_floor_dir_clean_any_time_supported=False, is_pet_supplies_deep_clean_supported=False, is_mop_shake_water_max_supported=False, is_exact_custom_mode_supported=False, is_video_patrol_supported=False, is_carpet_custom_clean_supported=False, is_pet_snapshot_supported=False, is_custom_clean_mode_count_supported=False, is_new_ai_recognition_supported=False, is_auto_collection_2_supported=False, is_right_brush_stretch_supported=False, is_smart_clean_mode_set_supported=False, is_dirty_object_detect_supported=False, is_no_need_carpet_press_set_supported=False, is_voice_control_led_supported=False, is_water_leak_check_supported=False, is_min_battery_15_to_clean_task_supported=False, is_gap_deep_clean_supported=False, is_object_detect_check_supported=False, is_identify_room_supported=False, is_matter_supported=False, is_workday_holiday_supported=False, is_clean_direct_status_supported=False, is_map_eraser_supported=False, is_optimize_battery_supported=False, is_activate_video_charging_and_standby_supported=False, is_carpet_long_haired_supported=False, is_clean_history_time_line_supported=False, is_max_zone_opened_supported=False, is_exhibition_function_supported=False, is_lds_lifting_supported=False, is_auto_tear_down_mop_supported=False, is_small_side_mop_supported=False, is_support_side_brush_up_down_supported=False, is_dry_interval_timer_supported=False, is_uvc_sterilize_supported=False, is_midway_back_to_dock_supported=False, is_support_main_brush_up_down_supported=False, is_egg_dance_mode_supported=False, is_mechanical_arm_mode_supported=False, is_tidyup_zones_supported=False, is_clean_time_line_supported=False, is_clean_then_mop_mode_supported=False, is_type_identify_supported=False, is_support_get_particular_status_supported=False, is_three_d_mapping_inner_test_supported=False, is_sync_server_name_supported=False, is_should_show_arm_over_load_supported=False, is_collect_dust_count_show_supported=False, is_support_api_app_stop_grasp_supported=False, is_ctm_with_repeat_supported=False, is_side_brush_lift_carpet_supported=False, is_detect_wire_carpet_supported=False, is_water_slide_mode_supported=False, is_soak_and_wash_supported=False, is_clean_efficiency_supported=False, is_back_wash_new_smart_supported=False, is_dual_band_wi_fi_supported=False, is_program_mode_supported=False, is_clean_fluid_delivery_supported=False, is_carpet_long_haired_ex_supported=False, is_over_sea_ctm_supported=False, is_full_duples_switch_supported=False, is_low_area_access_supported=False, is_follow_low_obs_supported=False, is_two_gears_no_collision_supported=False, is_carpet_shape_type_supported=False, is_sr_map_supported=False, is_led_status_switch_supported=False, is_multi_floor_supported=False, is_support_fetch_timer_summary=False, is_order_clean_supported=False, is_analysis_supported=False, is_remote_supported=False, is_support_voice_control_debug=False, is_mop_forbidden_supported=True, is_soft_clean_mode_supported=True, is_custom_mode_supported=True, is_support_custom_carpet=True, is_show_general_obstacle_supported=True, is_show_obstacle_photo_supported=True, is_rubber_brush_carpet_supported=True, is_carpet_pressure_use_origin_paras_supported=True, is_support_mop_back_pwm_set=True, is_collect_dust_mode_supported=True, is_support_water_mode=False, is_pure_clean_mop_supported=False, is_new_remote_view_supported=False, is_max_plus_mode_supported=False, is_none_pure_clean_mop_with_max_plus=False, is_clean_route_setting_supported=False, is_mop_shake_module_supported=False, is_customized_clean_supported=False), trait_data={'test_trait': {'key': 'value', 'number': 42}}), 'device2': DeviceCacheData(network_info=None, home_map_info=None, home_map_content_base64=None, device_features=None, trait_data=None)}, network_info={}, home_map_info={}, home_map_content={}, home_map_content_base64={}, device_features=None, trait_data=None) + CacheData(home_data=HomeData(id=123456, name='My Home', products=[HomeDataProduct(id='product-id-s7-maxv', name='Roborock S7 MaxV', model='roborock.vacuum.a27', category=, code='a27', icon_url=None, attribute=None, capability=0, schema=[HomeDataProductSchema(id='101', name='rpc_request', code='rpc_request_code', mode='rw', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='102', name='rpc_response', code='rpc_response', mode='rw', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='120', name='错误代码', code='error_code', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='121', name='设备状态', code='state', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='122', name='设备电量', code='battery', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='123', name='清扫模式', code='fan_power', mode='rw', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='124', name='拖地模式', code='water_box_mode', mode='rw', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='125', name='主刷寿命', code='main_brush_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='126', name='边刷寿命', code='side_brush_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='127', name='滤网寿命', code='filter_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='128', name='额外状态', code='additional_props', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='130', name='完成事件', code='task_complete', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='131', name='电量不足任务取消', code='task_cancel_low_power', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='132', name='运动中任务取消', code='task_cancel_in_motion', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='133', name='充电状态', code='charge_status', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='134', name='烘干状态', code='drying_status', mode='ro', type='RAW', product_property=None, property=None, desc=None)])], devices=[HomeDataDevice(duid='abc123', name='Roborock S7 MaxV', local_key='key123key123key1', product_id='product-id-s7-maxv', fv='02.56.02', attribute=None, active_time=1672364449, runtime_env=None, time_zone_id='America/Los_Angeles', icon_url='no_url', lon=None, lat=None, share=False, share_time=None, online=True, pv='1.0', room_id=2362003, tuya_uuid=None, tuya_migrated=False, extra='{"RRPhotoPrivacyVersion": "1"}', sn='abc123', feature_set='2234201184108543', new_feature_set='0000000000002041', device_status={'121': 8, '122': 100, '123': 102, '124': 203, '125': 94, '126': 90, '127': 87, '128': 0, '133': 1, '120': 0}, silent_ota_switch=True, setting=None, f=None, create_time=None, cid=None, share_type=None, share_expired_time=None)], received_devices=[], lon=None, lat=None, geo_name=None, rooms=[HomeDataRoom(id=2362048, name='Example room 1'), HomeDataRoom(id=2362044, name='Example room 2'), HomeDataRoom(id=2362041, name='Example room 3')]), device_info={'device1': DeviceCacheData(network_info=NetworkInfo(ip='1.1.1.1', ssid='test_wifi', mac='aa:bb:cc:dd:ee:ff', bssid='aa:bb:cc:dd:ee:ff', rssi=-50), home_map_info={1: CombinedMapInfo(map_flag=1, name='Test Map 1', rooms=[NamedRoomMapping(segment_id=1023, iot_id='4321', raw_name='Living Room'), NamedRoomMapping(segment_id=1024, iot_id='4322', raw_name='Starship')]), 2: CombinedMapInfo(map_flag=2, name='Test Map 2', rooms=[NamedRoomMapping(segment_id=2047, iot_id='5432', raw_name='Bedroom')])}, home_map_content_base64={1: 'ZHVtbXlfbWFwX2NvbnRlbnQ=', 2: 'bW9yZV9kdW1teV9jb250ZW50'}, device_features=DeviceFeatures(is_show_clean_finish_reason_supported=True, is_re_segment_supported=False, is_video_monitor_supported=False, is_any_state_transit_goto_supported=False, is_fw_filter_obstacle_supported=False, is_video_setting_supported=False, is_ignore_unknown_map_object_supported=False, is_set_child_supported=False, is_carpet_supported=False, is_record_allowed=False, is_mop_path_supported=False, is_multi_map_segment_timer_supported=False, is_current_map_restore_enabled=False, is_room_name_supported=False, is_shake_mop_set_supported=False, is_map_beautify_internal_debug_supported=False, is_new_data_for_clean_history=False, is_new_data_for_clean_history_detail=False, is_flow_led_setting_supported=False, is_dust_collection_setting_supported=False, is_rpc_retry_supported=False, is_avoid_collision_supported=False, is_support_set_switch_map_mode=False, is_map_carpet_add_support=False, is_custom_water_box_distance_supported=False, is_support_smart_scene=False, is_support_floor_edit=False, is_support_furniture=False, is_wash_then_charge_cmd_supported=False, is_support_room_tag=False, is_support_quick_map_builder=False, is_support_smart_global_clean_with_custom_mode=False, is_careful_slow_mop_supported=False, is_egg_mode_supported_from_new_features=False, is_carpet_show_on_map=False, is_supported_valley_electricity=False, is_unsave_map_reason_supported=False, is_supported_drying=False, is_supported_download_test_voice=False, is_support_backup_map=False, is_support_custom_mode_in_cleaning=False, is_support_remote_control_in_call=False, is_support_set_volume_in_call=True, is_support_clean_estimate=False, is_support_custom_dnd=False, is_carpet_deep_clean_supported=False, is_support_stuck_zone=False, is_support_custom_door_sill=False, is_wifi_manage_supported=False, is_clean_route_fast_mode_supported=False, is_support_cliff_zone=False, is_support_smart_door_sill=False, is_support_floor_direction=False, is_back_charge_auto_wash_supported=False, is_support_incremental_map=False, is_offline_map_supported=False, is_super_deep_wash_supported=False, is_ces_2022_supported=False, is_dss_believable=False, is_main_brush_up_down_supported_from_str=False, is_goto_pure_clean_path_supported=False, is_water_up_down_drain_supported=False, is_setting_carpet_first_supported=False, is_clean_route_deep_slow_plus_supported=False, is_dynamically_skip_clean_zone_supported=False, is_dynamically_add_clean_zones_supported=False, is_left_water_drain_supported=False, is_clean_count_setting_supported=False, is_corner_clean_mode_supported=False, is_two_key_real_time_video_supported=False, is_two_key_rtv_in_charging_supported=False, is_dirty_replenish_clean_supported=False, is_auto_delivery_field_in_global_status_supported=False, is_avoid_collision_mode_supported=False, is_voice_control_supported=False, is_new_endpoint_supported=False, is_pumping_water_supported=False, is_corner_mop_stretch_supported=False, is_hot_wash_towel_supported=False, is_floor_dir_clean_any_time_supported=False, is_pet_supplies_deep_clean_supported=False, is_mop_shake_water_max_supported=False, is_exact_custom_mode_supported=False, is_video_patrol_supported=False, is_carpet_custom_clean_supported=False, is_pet_snapshot_supported=False, is_custom_clean_mode_count_supported=False, is_new_ai_recognition_supported=False, is_auto_collection_2_supported=False, is_right_brush_stretch_supported=False, is_smart_clean_mode_set_supported=False, is_dirty_object_detect_supported=False, is_no_need_carpet_press_set_supported=False, is_voice_control_led_supported=False, is_water_leak_check_supported=False, is_min_battery_15_to_clean_task_supported=False, is_gap_deep_clean_supported=False, is_object_detect_check_supported=False, is_identify_room_supported=False, is_matter_supported=False, is_workday_holiday_supported=False, is_clean_direct_status_supported=False, is_map_eraser_supported=False, is_optimize_battery_supported=False, is_activate_video_charging_and_standby_supported=False, is_carpet_long_haired_supported=False, is_clean_history_time_line_supported=False, is_max_zone_opened_supported=False, is_exhibition_function_supported=False, is_lds_lifting_supported=False, is_auto_tear_down_mop_supported=False, is_small_side_mop_supported=False, is_support_side_brush_up_down_supported=False, is_dry_interval_timer_supported=False, is_uvc_sterilize_supported=False, is_midway_back_to_dock_supported=False, is_support_main_brush_up_down_supported=False, is_egg_dance_mode_supported=False, is_mechanical_arm_mode_supported=False, is_tidyup_zones_supported=False, is_clean_time_line_supported=False, is_clean_then_mop_mode_supported=False, is_type_identify_supported=False, is_support_get_particular_status_supported=False, is_three_d_mapping_inner_test_supported=False, is_sync_server_name_supported=False, is_should_show_arm_over_load_supported=False, is_collect_dust_count_show_supported=False, is_support_api_app_stop_grasp_supported=False, is_ctm_with_repeat_supported=False, is_side_brush_lift_carpet_supported=False, is_detect_wire_carpet_supported=False, is_water_slide_mode_supported=False, is_soak_and_wash_supported=False, is_clean_efficiency_supported=False, is_back_wash_new_smart_supported=False, is_dual_band_wi_fi_supported=False, is_program_mode_supported=False, is_clean_fluid_delivery_supported=False, is_carpet_long_haired_ex_supported=False, is_over_sea_ctm_supported=False, is_full_duples_switch_supported=False, is_low_area_access_supported=False, is_follow_low_obs_supported=False, is_two_gears_no_collision_supported=False, is_carpet_shape_type_supported=False, is_sr_map_supported=False, is_led_status_switch_supported=False, is_multi_floor_supported=False, is_support_fetch_timer_summary=False, is_order_clean_supported=False, is_analysis_supported=False, is_remote_supported=False, is_support_voice_control_debug=False, is_mop_forbidden_supported=True, is_soft_clean_mode_supported=True, is_custom_mode_supported=True, is_support_custom_carpet=True, is_show_general_obstacle_supported=True, is_show_obstacle_photo_supported=True, is_rubber_brush_carpet_supported=True, is_carpet_pressure_use_origin_paras_supported=True, is_support_mop_back_pwm_set=True, is_collect_dust_mode_supported=True, is_support_water_mode=False, is_ai_recognition_setting_supported=False, is_ai_recognition_obstacle_supported=False, is_pure_clean_mop_supported=False, is_new_remote_view_supported=False, is_max_plus_mode_supported=False, is_none_pure_clean_mop_with_max_plus=False, is_clean_route_setting_supported=False, is_mop_shake_module_supported=False, is_customized_clean_supported=False), trait_data={'test_trait': {'key': 'value', 'number': 42}}), 'device2': DeviceCacheData(network_info=None, home_map_info=None, home_map_content_base64=None, device_features=None, trait_data=None)}, network_info={}, home_map_info={}, home_map_content={}, home_map_content_base64={}, device_features=None, trait_data=None) # --- # name: test_set_and_flush_and_get[json-empty_cache] CacheData(home_data=None, device_info={}, network_info={}, home_map_info={}, home_map_content={}, home_map_content_base64={}, device_features=None, trait_data=None) @@ -24,7 +24,7 @@ CacheData(home_data=HomeData(id=123456, name='My Home', products=[HomeDataProduct(id='product-id-s7-maxv', name='Roborock S7 MaxV', model='roborock.vacuum.a27', category=, code='a27', icon_url=None, attribute=None, capability=0, schema=[HomeDataProductSchema(id='101', name='rpc_request', code='rpc_request_code', mode='rw', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='102', name='rpc_response', code='rpc_response', mode='rw', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='120', name='错误代码', code='error_code', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='121', name='设备状态', code='state', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='122', name='设备电量', code='battery', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='123', name='清扫模式', code='fan_power', mode='rw', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='124', name='拖地模式', code='water_box_mode', mode='rw', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='125', name='主刷寿命', code='main_brush_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='126', name='边刷寿命', code='side_brush_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='127', name='滤网寿命', code='filter_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='128', name='额外状态', code='additional_props', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='130', name='完成事件', code='task_complete', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='131', name='电量不足任务取消', code='task_cancel_low_power', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='132', name='运动中任务取消', code='task_cancel_in_motion', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='133', name='充电状态', code='charge_status', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='134', name='烘干状态', code='drying_status', mode='ro', type='RAW', product_property=None, property=None, desc=None)])], devices=[HomeDataDevice(duid='abc123', name='Roborock S7 MaxV', local_key='key123key123key1', product_id='product-id-s7-maxv', fv='02.56.02', attribute=None, active_time=1672364449, runtime_env=None, time_zone_id='America/Los_Angeles', icon_url='no_url', lon=None, lat=None, share=False, share_time=None, online=True, pv='1.0', room_id=2362003, tuya_uuid=None, tuya_migrated=False, extra='{"RRPhotoPrivacyVersion": "1"}', sn='abc123', feature_set='2234201184108543', new_feature_set='0000000000002041', device_status={'121': 8, '122': 100, '123': 102, '124': 203, '125': 94, '126': 90, '127': 87, '128': 0, '133': 1, '120': 0}, silent_ota_switch=True, setting=None, f=None, create_time=None, cid=None, share_type=None, share_expired_time=None)], received_devices=[], lon=None, lat=None, geo_name=None, rooms=[HomeDataRoom(id=2362048, name='Example room 1'), HomeDataRoom(id=2362044, name='Example room 2'), HomeDataRoom(id=2362041, name='Example room 3')]), device_info={}, network_info={}, home_map_info={}, home_map_content={}, home_map_content_base64={}, device_features=None, trait_data=None) # --- # name: test_set_and_flush_and_get[pickle-all_fields_cache] - CacheData(home_data=HomeData(id=123456, name='My Home', products=[HomeDataProduct(id='product-id-s7-maxv', name='Roborock S7 MaxV', model='roborock.vacuum.a27', category=, code='a27', icon_url=None, attribute=None, capability=0, schema=[HomeDataProductSchema(id='101', name='rpc_request', code='rpc_request_code', mode='rw', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='102', name='rpc_response', code='rpc_response', mode='rw', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='120', name='错误代码', code='error_code', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='121', name='设备状态', code='state', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='122', name='设备电量', code='battery', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='123', name='清扫模式', code='fan_power', mode='rw', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='124', name='拖地模式', code='water_box_mode', mode='rw', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='125', name='主刷寿命', code='main_brush_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='126', name='边刷寿命', code='side_brush_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='127', name='滤网寿命', code='filter_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='128', name='额外状态', code='additional_props', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='130', name='完成事件', code='task_complete', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='131', name='电量不足任务取消', code='task_cancel_low_power', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='132', name='运动中任务取消', code='task_cancel_in_motion', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='133', name='充电状态', code='charge_status', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='134', name='烘干状态', code='drying_status', mode='ro', type='RAW', product_property=None, property=None, desc=None)])], devices=[HomeDataDevice(duid='abc123', name='Roborock S7 MaxV', local_key='key123key123key1', product_id='product-id-s7-maxv', fv='02.56.02', attribute=None, active_time=1672364449, runtime_env=None, time_zone_id='America/Los_Angeles', icon_url='no_url', lon=None, lat=None, share=False, share_time=None, online=True, pv='1.0', room_id=2362003, tuya_uuid=None, tuya_migrated=False, extra='{"RRPhotoPrivacyVersion": "1"}', sn='abc123', feature_set='2234201184108543', new_feature_set='0000000000002041', device_status={'121': 8, '122': 100, '123': 102, '124': 203, '125': 94, '126': 90, '127': 87, '128': 0, '133': 1, '120': 0}, silent_ota_switch=True, setting=None, f=None, create_time=None, cid=None, share_type=None, share_expired_time=None)], received_devices=[], lon=None, lat=None, geo_name=None, rooms=[HomeDataRoom(id=2362048, name='Example room 1'), HomeDataRoom(id=2362044, name='Example room 2'), HomeDataRoom(id=2362041, name='Example room 3')]), device_info={'device1': DeviceCacheData(network_info=NetworkInfo(ip='1.1.1.1', ssid='test_wifi', mac='aa:bb:cc:dd:ee:ff', bssid='aa:bb:cc:dd:ee:ff', rssi=-50), home_map_info={1: CombinedMapInfo(map_flag=1, name='Test Map 1', rooms=[NamedRoomMapping(segment_id=1023, iot_id='4321', raw_name='Living Room'), NamedRoomMapping(segment_id=1024, iot_id='4322', raw_name='Starship')]), 2: CombinedMapInfo(map_flag=2, name='Test Map 2', rooms=[NamedRoomMapping(segment_id=2047, iot_id='5432', raw_name='Bedroom')])}, home_map_content_base64={1: 'ZHVtbXlfbWFwX2NvbnRlbnQ=', 2: 'bW9yZV9kdW1teV9jb250ZW50'}, device_features=DeviceFeatures(is_show_clean_finish_reason_supported=True, is_re_segment_supported=False, is_video_monitor_supported=False, is_any_state_transit_goto_supported=False, is_fw_filter_obstacle_supported=False, is_video_setting_supported=False, is_ignore_unknown_map_object_supported=False, is_set_child_supported=False, is_carpet_supported=False, is_record_allowed=False, is_mop_path_supported=False, is_multi_map_segment_timer_supported=False, is_current_map_restore_enabled=False, is_room_name_supported=False, is_shake_mop_set_supported=False, is_map_beautify_internal_debug_supported=False, is_new_data_for_clean_history=False, is_new_data_for_clean_history_detail=False, is_flow_led_setting_supported=False, is_dust_collection_setting_supported=False, is_rpc_retry_supported=False, is_avoid_collision_supported=False, is_support_set_switch_map_mode=False, is_map_carpet_add_support=False, is_custom_water_box_distance_supported=False, is_support_smart_scene=False, is_support_floor_edit=False, is_support_furniture=False, is_wash_then_charge_cmd_supported=False, is_support_room_tag=False, is_support_quick_map_builder=False, is_support_smart_global_clean_with_custom_mode=False, is_careful_slow_mop_supported=False, is_egg_mode_supported_from_new_features=False, is_carpet_show_on_map=False, is_supported_valley_electricity=False, is_unsave_map_reason_supported=False, is_supported_drying=False, is_supported_download_test_voice=False, is_support_backup_map=False, is_support_custom_mode_in_cleaning=False, is_support_remote_control_in_call=False, is_support_set_volume_in_call=True, is_support_clean_estimate=False, is_support_custom_dnd=False, is_carpet_deep_clean_supported=False, is_support_stuck_zone=False, is_support_custom_door_sill=False, is_wifi_manage_supported=False, is_clean_route_fast_mode_supported=False, is_support_cliff_zone=False, is_support_smart_door_sill=False, is_support_floor_direction=False, is_back_charge_auto_wash_supported=False, is_support_incremental_map=False, is_offline_map_supported=False, is_super_deep_wash_supported=False, is_ces_2022_supported=False, is_dss_believable=False, is_main_brush_up_down_supported_from_str=False, is_goto_pure_clean_path_supported=False, is_water_up_down_drain_supported=False, is_setting_carpet_first_supported=False, is_clean_route_deep_slow_plus_supported=False, is_dynamically_skip_clean_zone_supported=False, is_dynamically_add_clean_zones_supported=False, is_left_water_drain_supported=False, is_clean_count_setting_supported=False, is_corner_clean_mode_supported=False, is_two_key_real_time_video_supported=False, is_two_key_rtv_in_charging_supported=False, is_dirty_replenish_clean_supported=False, is_auto_delivery_field_in_global_status_supported=False, is_avoid_collision_mode_supported=False, is_voice_control_supported=False, is_new_endpoint_supported=False, is_pumping_water_supported=False, is_corner_mop_stretch_supported=False, is_hot_wash_towel_supported=False, is_floor_dir_clean_any_time_supported=False, is_pet_supplies_deep_clean_supported=False, is_mop_shake_water_max_supported=False, is_exact_custom_mode_supported=False, is_video_patrol_supported=False, is_carpet_custom_clean_supported=False, is_pet_snapshot_supported=False, is_custom_clean_mode_count_supported=False, is_new_ai_recognition_supported=False, is_auto_collection_2_supported=False, is_right_brush_stretch_supported=False, is_smart_clean_mode_set_supported=False, is_dirty_object_detect_supported=False, is_no_need_carpet_press_set_supported=False, is_voice_control_led_supported=False, is_water_leak_check_supported=False, is_min_battery_15_to_clean_task_supported=False, is_gap_deep_clean_supported=False, is_object_detect_check_supported=False, is_identify_room_supported=False, is_matter_supported=False, is_workday_holiday_supported=False, is_clean_direct_status_supported=False, is_map_eraser_supported=False, is_optimize_battery_supported=False, is_activate_video_charging_and_standby_supported=False, is_carpet_long_haired_supported=False, is_clean_history_time_line_supported=False, is_max_zone_opened_supported=False, is_exhibition_function_supported=False, is_lds_lifting_supported=False, is_auto_tear_down_mop_supported=False, is_small_side_mop_supported=False, is_support_side_brush_up_down_supported=False, is_dry_interval_timer_supported=False, is_uvc_sterilize_supported=False, is_midway_back_to_dock_supported=False, is_support_main_brush_up_down_supported=False, is_egg_dance_mode_supported=False, is_mechanical_arm_mode_supported=False, is_tidyup_zones_supported=False, is_clean_time_line_supported=False, is_clean_then_mop_mode_supported=False, is_type_identify_supported=False, is_support_get_particular_status_supported=False, is_three_d_mapping_inner_test_supported=False, is_sync_server_name_supported=False, is_should_show_arm_over_load_supported=False, is_collect_dust_count_show_supported=False, is_support_api_app_stop_grasp_supported=False, is_ctm_with_repeat_supported=False, is_side_brush_lift_carpet_supported=False, is_detect_wire_carpet_supported=False, is_water_slide_mode_supported=False, is_soak_and_wash_supported=False, is_clean_efficiency_supported=False, is_back_wash_new_smart_supported=False, is_dual_band_wi_fi_supported=False, is_program_mode_supported=False, is_clean_fluid_delivery_supported=False, is_carpet_long_haired_ex_supported=False, is_over_sea_ctm_supported=False, is_full_duples_switch_supported=False, is_low_area_access_supported=False, is_follow_low_obs_supported=False, is_two_gears_no_collision_supported=False, is_carpet_shape_type_supported=False, is_sr_map_supported=False, is_led_status_switch_supported=False, is_multi_floor_supported=False, is_support_fetch_timer_summary=False, is_order_clean_supported=False, is_analysis_supported=False, is_remote_supported=False, is_support_voice_control_debug=False, is_mop_forbidden_supported=True, is_soft_clean_mode_supported=True, is_custom_mode_supported=True, is_support_custom_carpet=True, is_show_general_obstacle_supported=True, is_show_obstacle_photo_supported=True, is_rubber_brush_carpet_supported=True, is_carpet_pressure_use_origin_paras_supported=True, is_support_mop_back_pwm_set=True, is_collect_dust_mode_supported=True, is_support_water_mode=False, is_pure_clean_mop_supported=False, is_new_remote_view_supported=False, is_max_plus_mode_supported=False, is_none_pure_clean_mop_with_max_plus=False, is_clean_route_setting_supported=False, is_mop_shake_module_supported=False, is_customized_clean_supported=False), trait_data={'test_trait': {'key': 'value', 'number': 42}}), 'device2': DeviceCacheData(network_info=None, home_map_info=None, home_map_content_base64=None, device_features=None, trait_data=None)}, network_info={}, home_map_info={}, home_map_content={}, home_map_content_base64={}, device_features=None, trait_data=None) + CacheData(home_data=HomeData(id=123456, name='My Home', products=[HomeDataProduct(id='product-id-s7-maxv', name='Roborock S7 MaxV', model='roborock.vacuum.a27', category=, code='a27', icon_url=None, attribute=None, capability=0, schema=[HomeDataProductSchema(id='101', name='rpc_request', code='rpc_request_code', mode='rw', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='102', name='rpc_response', code='rpc_response', mode='rw', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='120', name='错误代码', code='error_code', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='121', name='设备状态', code='state', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='122', name='设备电量', code='battery', mode='ro', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='123', name='清扫模式', code='fan_power', mode='rw', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='124', name='拖地模式', code='water_box_mode', mode='rw', type='ENUM', product_property=None, property='{"range": []}', desc=None), HomeDataProductSchema(id='125', name='主刷寿命', code='main_brush_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='126', name='边刷寿命', code='side_brush_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='127', name='滤网寿命', code='filter_life', mode='rw', type='VALUE', product_property=None, property='{"max": 100, "min": 0, "step": 1, "unit": null, "scale": 1}', desc=None), HomeDataProductSchema(id='128', name='额外状态', code='additional_props', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='130', name='完成事件', code='task_complete', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='131', name='电量不足任务取消', code='task_cancel_low_power', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='132', name='运动中任务取消', code='task_cancel_in_motion', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='133', name='充电状态', code='charge_status', mode='ro', type='RAW', product_property=None, property=None, desc=None), HomeDataProductSchema(id='134', name='烘干状态', code='drying_status', mode='ro', type='RAW', product_property=None, property=None, desc=None)])], devices=[HomeDataDevice(duid='abc123', name='Roborock S7 MaxV', local_key='key123key123key1', product_id='product-id-s7-maxv', fv='02.56.02', attribute=None, active_time=1672364449, runtime_env=None, time_zone_id='America/Los_Angeles', icon_url='no_url', lon=None, lat=None, share=False, share_time=None, online=True, pv='1.0', room_id=2362003, tuya_uuid=None, tuya_migrated=False, extra='{"RRPhotoPrivacyVersion": "1"}', sn='abc123', feature_set='2234201184108543', new_feature_set='0000000000002041', device_status={'121': 8, '122': 100, '123': 102, '124': 203, '125': 94, '126': 90, '127': 87, '128': 0, '133': 1, '120': 0}, silent_ota_switch=True, setting=None, f=None, create_time=None, cid=None, share_type=None, share_expired_time=None)], received_devices=[], lon=None, lat=None, geo_name=None, rooms=[HomeDataRoom(id=2362048, name='Example room 1'), HomeDataRoom(id=2362044, name='Example room 2'), HomeDataRoom(id=2362041, name='Example room 3')]), device_info={'device1': DeviceCacheData(network_info=NetworkInfo(ip='1.1.1.1', ssid='test_wifi', mac='aa:bb:cc:dd:ee:ff', bssid='aa:bb:cc:dd:ee:ff', rssi=-50), home_map_info={1: CombinedMapInfo(map_flag=1, name='Test Map 1', rooms=[NamedRoomMapping(segment_id=1023, iot_id='4321', raw_name='Living Room'), NamedRoomMapping(segment_id=1024, iot_id='4322', raw_name='Starship')]), 2: CombinedMapInfo(map_flag=2, name='Test Map 2', rooms=[NamedRoomMapping(segment_id=2047, iot_id='5432', raw_name='Bedroom')])}, home_map_content_base64={1: 'ZHVtbXlfbWFwX2NvbnRlbnQ=', 2: 'bW9yZV9kdW1teV9jb250ZW50'}, device_features=DeviceFeatures(is_show_clean_finish_reason_supported=True, is_re_segment_supported=False, is_video_monitor_supported=False, is_any_state_transit_goto_supported=False, is_fw_filter_obstacle_supported=False, is_video_setting_supported=False, is_ignore_unknown_map_object_supported=False, is_set_child_supported=False, is_carpet_supported=False, is_record_allowed=False, is_mop_path_supported=False, is_multi_map_segment_timer_supported=False, is_current_map_restore_enabled=False, is_room_name_supported=False, is_shake_mop_set_supported=False, is_map_beautify_internal_debug_supported=False, is_new_data_for_clean_history=False, is_new_data_for_clean_history_detail=False, is_flow_led_setting_supported=False, is_dust_collection_setting_supported=False, is_rpc_retry_supported=False, is_avoid_collision_supported=False, is_support_set_switch_map_mode=False, is_map_carpet_add_support=False, is_custom_water_box_distance_supported=False, is_support_smart_scene=False, is_support_floor_edit=False, is_support_furniture=False, is_wash_then_charge_cmd_supported=False, is_support_room_tag=False, is_support_quick_map_builder=False, is_support_smart_global_clean_with_custom_mode=False, is_careful_slow_mop_supported=False, is_egg_mode_supported_from_new_features=False, is_carpet_show_on_map=False, is_supported_valley_electricity=False, is_unsave_map_reason_supported=False, is_supported_drying=False, is_supported_download_test_voice=False, is_support_backup_map=False, is_support_custom_mode_in_cleaning=False, is_support_remote_control_in_call=False, is_support_set_volume_in_call=True, is_support_clean_estimate=False, is_support_custom_dnd=False, is_carpet_deep_clean_supported=False, is_support_stuck_zone=False, is_support_custom_door_sill=False, is_wifi_manage_supported=False, is_clean_route_fast_mode_supported=False, is_support_cliff_zone=False, is_support_smart_door_sill=False, is_support_floor_direction=False, is_back_charge_auto_wash_supported=False, is_support_incremental_map=False, is_offline_map_supported=False, is_super_deep_wash_supported=False, is_ces_2022_supported=False, is_dss_believable=False, is_main_brush_up_down_supported_from_str=False, is_goto_pure_clean_path_supported=False, is_water_up_down_drain_supported=False, is_setting_carpet_first_supported=False, is_clean_route_deep_slow_plus_supported=False, is_dynamically_skip_clean_zone_supported=False, is_dynamically_add_clean_zones_supported=False, is_left_water_drain_supported=False, is_clean_count_setting_supported=False, is_corner_clean_mode_supported=False, is_two_key_real_time_video_supported=False, is_two_key_rtv_in_charging_supported=False, is_dirty_replenish_clean_supported=False, is_auto_delivery_field_in_global_status_supported=False, is_avoid_collision_mode_supported=False, is_voice_control_supported=False, is_new_endpoint_supported=False, is_pumping_water_supported=False, is_corner_mop_stretch_supported=False, is_hot_wash_towel_supported=False, is_floor_dir_clean_any_time_supported=False, is_pet_supplies_deep_clean_supported=False, is_mop_shake_water_max_supported=False, is_exact_custom_mode_supported=False, is_video_patrol_supported=False, is_carpet_custom_clean_supported=False, is_pet_snapshot_supported=False, is_custom_clean_mode_count_supported=False, is_new_ai_recognition_supported=False, is_auto_collection_2_supported=False, is_right_brush_stretch_supported=False, is_smart_clean_mode_set_supported=False, is_dirty_object_detect_supported=False, is_no_need_carpet_press_set_supported=False, is_voice_control_led_supported=False, is_water_leak_check_supported=False, is_min_battery_15_to_clean_task_supported=False, is_gap_deep_clean_supported=False, is_object_detect_check_supported=False, is_identify_room_supported=False, is_matter_supported=False, is_workday_holiday_supported=False, is_clean_direct_status_supported=False, is_map_eraser_supported=False, is_optimize_battery_supported=False, is_activate_video_charging_and_standby_supported=False, is_carpet_long_haired_supported=False, is_clean_history_time_line_supported=False, is_max_zone_opened_supported=False, is_exhibition_function_supported=False, is_lds_lifting_supported=False, is_auto_tear_down_mop_supported=False, is_small_side_mop_supported=False, is_support_side_brush_up_down_supported=False, is_dry_interval_timer_supported=False, is_uvc_sterilize_supported=False, is_midway_back_to_dock_supported=False, is_support_main_brush_up_down_supported=False, is_egg_dance_mode_supported=False, is_mechanical_arm_mode_supported=False, is_tidyup_zones_supported=False, is_clean_time_line_supported=False, is_clean_then_mop_mode_supported=False, is_type_identify_supported=False, is_support_get_particular_status_supported=False, is_three_d_mapping_inner_test_supported=False, is_sync_server_name_supported=False, is_should_show_arm_over_load_supported=False, is_collect_dust_count_show_supported=False, is_support_api_app_stop_grasp_supported=False, is_ctm_with_repeat_supported=False, is_side_brush_lift_carpet_supported=False, is_detect_wire_carpet_supported=False, is_water_slide_mode_supported=False, is_soak_and_wash_supported=False, is_clean_efficiency_supported=False, is_back_wash_new_smart_supported=False, is_dual_band_wi_fi_supported=False, is_program_mode_supported=False, is_clean_fluid_delivery_supported=False, is_carpet_long_haired_ex_supported=False, is_over_sea_ctm_supported=False, is_full_duples_switch_supported=False, is_low_area_access_supported=False, is_follow_low_obs_supported=False, is_two_gears_no_collision_supported=False, is_carpet_shape_type_supported=False, is_sr_map_supported=False, is_led_status_switch_supported=False, is_multi_floor_supported=False, is_support_fetch_timer_summary=False, is_order_clean_supported=False, is_analysis_supported=False, is_remote_supported=False, is_support_voice_control_debug=False, is_mop_forbidden_supported=True, is_soft_clean_mode_supported=True, is_custom_mode_supported=True, is_support_custom_carpet=True, is_show_general_obstacle_supported=True, is_show_obstacle_photo_supported=True, is_rubber_brush_carpet_supported=True, is_carpet_pressure_use_origin_paras_supported=True, is_support_mop_back_pwm_set=True, is_collect_dust_mode_supported=True, is_support_water_mode=False, is_ai_recognition_setting_supported=False, is_ai_recognition_obstacle_supported=False, is_pure_clean_mop_supported=False, is_new_remote_view_supported=False, is_max_plus_mode_supported=False, is_none_pure_clean_mop_with_max_plus=False, is_clean_route_setting_supported=False, is_mop_shake_module_supported=False, is_customized_clean_supported=False), trait_data={'test_trait': {'key': 'value', 'number': 42}}), 'device2': DeviceCacheData(network_info=None, home_map_info=None, home_map_content_base64=None, device_features=None, trait_data=None)}, network_info={}, home_map_info={}, home_map_content={}, home_map_content_base64={}, device_features=None, trait_data=None) # --- # name: test_set_and_flush_and_get[pickle-empty_cache] CacheData(home_data=None, device_info={}, network_info={}, home_map_info={}, home_map_content={}, home_map_content_base64={}, device_features=None, trait_data=None) diff --git a/tests/devices/__snapshots__/test_v1_device.ambr b/tests/devices/__snapshots__/test_v1_device.ambr index 32f5c542..f33f2490 100644 --- a/tests/devices/__snapshots__/test_v1_device.ambr +++ b/tests/devices/__snapshots__/test_v1_device.ambr @@ -212,6 +212,8 @@ 125, ]), 'isActivateVideoChargingAndStandbySupported': False, + 'isAiRecognitionObstacleSupported': True, + 'isAiRecognitionSettingSupported': True, 'isAnalysisSupported': True, 'isAnyStateTransitGotoSupported': False, 'isAutoCollection2Supported': False, @@ -630,6 +632,8 @@ 125, ]), 'isActivateVideoChargingAndStandbySupported': False, + 'isAiRecognitionObstacleSupported': True, + 'isAiRecognitionSettingSupported': True, 'isAnalysisSupported': True, 'isAnyStateTransitGotoSupported': False, 'isAutoCollection2Supported': False, @@ -1055,6 +1059,8 @@ 125, ]), 'isActivateVideoChargingAndStandbySupported': False, + 'isAiRecognitionObstacleSupported': True, + 'isAiRecognitionSettingSupported': True, 'isAnalysisSupported': True, 'isAnyStateTransitGotoSupported': False, 'isAutoCollection2Supported': False, @@ -1450,6 +1456,8 @@ 125, ]), 'isActivateVideoChargingAndStandbySupported': False, + 'isAiRecognitionObstacleSupported': True, + 'isAiRecognitionSettingSupported': True, 'isAnalysisSupported': True, 'isAnyStateTransitGotoSupported': False, 'isAutoCollection2Supported': False, diff --git a/tests/devices/rpc/test_v1_channel.py b/tests/devices/rpc/test_v1_channel.py index 278a796b..6cc7a1f5 100644 --- a/tests/devices/rpc/test_v1_channel.py +++ b/tests/devices/rpc/test_v1_channel.py @@ -4,6 +4,7 @@ and failure modes, ensuring the V1Channel behaves correctly in various scenarios. """ +import gzip import json import logging from collections.abc import Iterator @@ -163,6 +164,12 @@ def setup_map_rpc_channel(v1_channel: V1Channel) -> V1RpcChannel: return v1_channel.map_rpc_channel +@pytest.fixture(name="blob_rpc_channel") +def setup_blob_rpc_channel(v1_channel: V1Channel) -> V1RpcChannel: + """Fixture to set up the Blob RPC channel for tests.""" + return v1_channel.blob_rpc_channel + + @pytest.fixture(name="warning_caplog") def setup_warning_caplog(caplog: pytest.LogCaptureFixture) -> pytest.LogCaptureFixture: """Fixture to capture warning messages.""" @@ -632,6 +639,75 @@ async def test_v1_channel_send_map_command( assert result == decompressed_map_data +async def test_v1_channel_send_blob_command( + blob_rpc_channel: V1RpcChannel, + mock_mqtt_channel: FakeChannel, +) -> None: + """Test that the blob channel adds security parameters and decodes the response.""" + public_key = {"n": "abc", "e": "010001"} + mock_mqtt_channel.response_queue.append( + RoborockMessage( + protocol=RoborockMessageProtocol.RPC_RESPONSE, + payload=json.dumps({"dps": {"102": json.dumps({"id": 12345, "result": {"pub_key": public_key}})}}).encode(), + ) + ) + + blob_data = b"blob response" + compressed = gzip.compress(blob_data) + header_size = 24 + payload = bytearray(header_size + len(compressed)) + payload[:8] = b"ROBOROCK" + payload[8:12] = (12346).to_bytes(4, "little") + payload[16:18] = header_size.to_bytes(2, "little") + payload[20:24] = len(compressed).to_bytes(4, "little") + payload[header_size:] = compressed + mock_mqtt_channel.response_queue.append( + RoborockMessage(protocol=RoborockMessageProtocol.MAP_RESPONSE, payload=bytes(payload)) + ) + + result = await blob_rpc_channel.send_command( + RoborockCommand.GET_PHOTO, + params={"img_id": "photo-id", "type": 1}, + ) + + assert result == blob_data + sent_payload = mock_mqtt_channel.published_messages[-1].payload + assert sent_payload is not None + request_payload = json.loads(sent_payload) + request = json.loads(request_payload["dps"]["101"]) + assert request["method"] == RoborockCommand.GET_PHOTO + assert request["params"] == { + "security": { + "pub_key": public_key, + "cipher_suite": 0, + }, + "endpoint": TEST_SECURITY_DATA.endpoint, + "nonce": TEST_SECURITY_DATA.nonce.hex(), + "data_filter": {"img_id": "photo-id", "type": 1}, + } + + +async def test_v1_channel_send_blob_command_rejects_invalid_public_key( + blob_rpc_channel: V1RpcChannel, + mock_mqtt_channel: FakeChannel, +) -> None: + """Test that the blob channel rejects an invalid public key response.""" + mock_mqtt_channel.response_queue.append( + RoborockMessage( + protocol=RoborockMessageProtocol.RPC_RESPONSE, + payload=json.dumps({"dps": {"102": json.dumps({"id": 12345, "result": {}})}}).encode(), + ) + ) + + with pytest.raises(RoborockException, match="did not contain a public key"): + await blob_rpc_channel.send_command( + RoborockCommand.GET_PHOTO, + params={"img_id": "photo-id", "type": 1}, + ) + + assert len(mock_mqtt_channel.published_messages) == 1 + + async def test_v1_channel_add_dps_listener( v1_channel: V1Channel, mock_mqtt_channel: FakeChannel, diff --git a/tests/devices/test_v1_device.py b/tests/devices/test_v1_device.py index c327bdf0..0f34d783 100644 --- a/tests/devices/test_v1_device.py +++ b/tests/devices/test_v1_device.py @@ -52,8 +52,16 @@ def map_rpc_channel_fixture() -> AsyncMock: return AsyncMock() +@pytest.fixture(autouse=True, name="blob_rpc_channel") +def blob_rpc_channel_fixture() -> AsyncMock: + """Fixture to set up the blob channel for tests.""" + return AsyncMock() + + @pytest.fixture(autouse=True, name="device") -def device_fixture(channel: AsyncMock, rpc_channel: AsyncMock, mqtt_rpc_channel: AsyncMock) -> RoborockDevice: +def device_fixture( + channel: AsyncMock, rpc_channel: AsyncMock, mqtt_rpc_channel: AsyncMock, blob_rpc_channel: AsyncMock +) -> RoborockDevice: """Fixture to set up the device for tests.""" return RoborockDevice( device_info=HOME_DATA.devices[0], @@ -66,6 +74,7 @@ def device_fixture(channel: AsyncMock, rpc_channel: AsyncMock, mqtt_rpc_channel: rpc_channel, mqtt_rpc_channel, AsyncMock(), + blob_rpc_channel, Mock(), AsyncMock(), device_cache=DeviceCache(HOME_DATA.devices[0].duid, NoCache()), @@ -259,6 +268,7 @@ async def test_connect_retries_after_transient_start_failure() -> None: AsyncMock(), AsyncMock(), AsyncMock(), + AsyncMock(), Mock(), AsyncMock(), device_cache=device_cache, diff --git a/tests/devices/traits/v1/fixtures.py b/tests/devices/traits/v1/fixtures.py index f73a9b11..2b062dbd 100644 --- a/tests/devices/traits/v1/fixtures.py +++ b/tests/devices/traits/v1/fixtures.py @@ -40,6 +40,12 @@ def map_rpc_channel_fixture() -> AsyncMock: return AsyncMock() +@pytest.fixture(autouse=True, name="mock_blob_rpc_channel") +def blob_rpc_channel_fixture() -> AsyncMock: + """Fixture to set up the blob channel for tests.""" + return AsyncMock() + + @pytest.fixture(autouse=True, name="web_api_client") def web_api_client_fixture() -> AsyncMock: """Fixture to set up the web API client for tests.""" @@ -82,6 +88,7 @@ def device_fixture( mock_rpc_channel: AsyncMock, mock_mqtt_rpc_channel: AsyncMock, mock_map_rpc_channel: AsyncMock, + mock_blob_rpc_channel: AsyncMock, web_api_client: AsyncMock, device_cache: DeviceCache, device_info: HomeDataDevice, @@ -101,6 +108,7 @@ def device_fixture( mock_rpc_channel, mock_mqtt_rpc_channel, mock_map_rpc_channel, + mock_blob_rpc_channel, Mock(), web_api_client, device_cache=device_cache, diff --git a/tests/devices/traits/v1/test_obstacle_photos.py b/tests/devices/traits/v1/test_obstacle_photos.py new file mode 100644 index 00000000..5597ec3e --- /dev/null +++ b/tests/devices/traits/v1/test_obstacle_photos.py @@ -0,0 +1,142 @@ +"""Tests for the ObstaclePhotoTrait.""" + +import gzip +from unittest.mock import AsyncMock, patch + +import pytest + +from roborock.devices.device import RoborockDevice +from roborock.devices.traits.v1.obstacle_photos import ( + ObstaclePhotoTrait, + parse_photo_data, +) +from roborock.exceptions import RoborockException +from roborock.protocol import Utils +from roborock.protocols.v1_protocol import create_blob_response_decoder +from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol +from roborock.roborock_typing import RoborockCommand + +PNG_BYTES = b"\x89PNG\r\n\x1a\nphoto-bytes" +JPEG_BYTES = b"\xff\xd8\xff\xe0photo-bytes" + + +def _block(block_type: int, payload: bytes, header_size: int = 8) -> bytes: + return ( + block_type.to_bytes(2, "little") + + header_size.to_bytes(2, "little") + + len(payload).to_bytes(4, "little") + + b"\0" * (header_size - 8) + + payload + ) + + +@pytest.fixture +def obstacle_photo_trait(device: RoborockDevice, mock_blob_rpc_channel: AsyncMock) -> ObstaclePhotoTrait: + """Create an ObstaclePhotoTrait instance with mocked dependencies.""" + assert device.v1_properties + trait = ObstaclePhotoTrait(device.v1_properties.status.rpc_channel) + trait._rpc_channel = mock_blob_rpc_channel + return trait + + +def test_parse_photo_data() -> None: + """Test parsing photo bytes from the get_photo typed payload.""" + assert parse_photo_data(_block(1, b"\x10\x00\x10\x00") + _block(3, PNG_BYTES)) == PNG_BYTES + + +def test_parse_photo_data_jpeg() -> None: + """Test parsing JPEG photo bytes from the get_photo typed payload.""" + assert parse_photo_data(_block(1, b"\x80\x02\xe0\x01") + _block(3, JPEG_BYTES)) == JPEG_BYTES + + +def test_parse_photo_data_with_extended_header() -> None: + """Test parsing a photo block whose header is larger than the minimum.""" + assert parse_photo_data(_block(3, PNG_BYTES, header_size=12)) == PNG_BYTES + + +def test_parse_photo_data_missing_photo() -> None: + """Test missing photo data block raises.""" + with pytest.raises(RoborockException): + parse_photo_data(_block(1, b"\x10\x00\x10\x00")) + + +def test_decode_blob_response() -> None: + """Test decoding the gzip blob frame used by obstacle photos.""" + request_id = 20008 + decompressed = _block(3, JPEG_BYTES) + compressed = gzip.compress(decompressed) + header_size = 56 + payload = bytearray(header_size + len(compressed)) + payload[:8] = b"ROBOROCK" + payload[8:12] = request_id.to_bytes(4, "little") + payload[16:18] = header_size.to_bytes(2, "little") + payload[20:24] = len(compressed).to_bytes(4, "little") + payload[header_size:] = compressed + + response = create_blob_response_decoder()( + RoborockMessage(protocol=RoborockMessageProtocol.MAP_RESPONSE, payload=bytes(payload)) + ) + + assert response is not None + assert response.request_id == request_id + assert response.data == decompressed + + +def test_decode_blob_response_wraps_decompression_errors() -> None: + """Test expected gzip decompression errors are wrapped.""" + payload = bytearray(25) + payload[:8] = b"ROBOROCK" + payload[16:18] = (24).to_bytes(2, "little") + payload[20:24] = (1).to_bytes(4, "little") + + with pytest.raises(RoborockException, match="Failed to decode blob message payload"): + create_blob_response_decoder()( + RoborockMessage(protocol=RoborockMessageProtocol.MAP_RESPONSE, payload=bytes(payload)) + ) + + +def test_decode_blob_response_does_not_wrap_unexpected_errors() -> None: + """Test unexpected decompression errors propagate unchanged.""" + payload = bytearray(25) + payload[:8] = b"ROBOROCK" + payload[16:18] = (24).to_bytes(2, "little") + payload[20:24] = (1).to_bytes(4, "little") + + with ( + patch.object(Utils, "decompress", side_effect=RuntimeError("unexpected")), + pytest.raises(RuntimeError, match="unexpected"), + ): + create_blob_response_decoder()( + RoborockMessage(protocol=RoborockMessageProtocol.MAP_RESPONSE, payload=bytes(payload)) + ) + + +def test_obstacle_photo_trait_metadata() -> None: + """Test obstacle photos use the blob RPC channel when discovered.""" + assert ObstaclePhotoTrait.blob_rpc_channel is True + assert ObstaclePhotoTrait.requires_feature == "is_ai_recognition_obstacle_supported" + + +async def test_get_enabled(obstacle_photo_trait: ObstaclePhotoTrait, mock_rpc_channel: AsyncMock) -> None: + """Test reading the app's map-object photo enabled bit from camera status.""" + mock_rpc_channel.send_command.return_value = [1 << 10] + + assert await obstacle_photo_trait.get_enabled() is True + mock_rpc_channel.send_command.assert_called_once_with(RoborockCommand.GET_CAMERA_STATUS) + + +async def test_get_photo( + obstacle_photo_trait: ObstaclePhotoTrait, + mock_blob_rpc_channel: AsyncMock, +) -> None: + """Test fetching and parsing obstacle photo content.""" + mock_blob_rpc_channel.send_command.return_value = _block(3, PNG_BYTES) + + photo = await obstacle_photo_trait.get_photo("photo-id") + + assert photo.photo_id == "photo-id" + assert photo.image_content == PNG_BYTES + mock_blob_rpc_channel.send_command.assert_called_once_with( + RoborockCommand.GET_PHOTO, + params={"img_id": "photo-id", "type": 1}, + )