diff --git a/roborock/data/v1/v1_clean_modes.py b/roborock/data/v1/v1_clean_modes.py index fabb414f..0fdfd140 100644 --- a/roborock/data/v1/v1_clean_modes.py +++ b/roborock/data/v1/v1_clean_modes.py @@ -16,6 +16,8 @@ class VacuumModes(RoborockModeEnum): TURBO = ("turbo", 103) MAX = ("max", 104) MAX_PLUS = ("max_plus", 108) + CARPET = ("carpet", 107) + OFF_RAISE_MAIN_BRUSH = ("off", 109) CUSTOMIZED = ("custom", 106) SMART_MODE = ("smart_mode", 110) @@ -45,6 +47,8 @@ class WaterModes(RoborockModeEnum): STANDARD = ("standard", 202) HIGH = ("high", 203) INTENSE = ("intense", 203) + MIN = ("min", 205) + MAX = ("max", 206) CUSTOMIZED = ("custom", 204) CUSTOM = ("custom_water_flow", 207) EXTREME = ("extreme", 208) @@ -81,9 +85,14 @@ def get_clean_modes(features: DeviceFeatures) -> list[VacuumModes]: if features.is_max_plus_mode_supported or features.is_none_pure_clean_mop_with_max_plus: # If the vacuum has max plus mode supported modes.append(VacuumModes.MAX_PLUS) + if features.is_carpet_deep_clean_supported: + modes.append(VacuumModes.CARPET) if features.is_pure_clean_mop_supported: # If the vacuum is capable of 'pure mop clean' aka no vacuum - modes.append(VacuumModes.OFF) + if features.is_support_main_brush_up_down_supported: + modes.append(VacuumModes.OFF_RAISE_MAIN_BRUSH) + else: + modes.append(VacuumModes.OFF) else: # If not, we can add gentle modes.append(VacuumModes.GENTLE) diff --git a/roborock/data/v1/v1_containers.py b/roborock/data/v1/v1_containers.py index 88560fe0..f13a6b8d 100644 --- a/roborock/data/v1/v1_containers.py +++ b/roborock/data/v1/v1_containers.py @@ -93,6 +93,8 @@ @dataclass class Status(RoborockBase): + """This status will be depreciated in favor of StatusV2.""" + msg_ver: int | None = None msg_seq: int | None = None state: RoborockStateCode | None = None @@ -254,6 +256,138 @@ def __repr__(self) -> str: return _attr_repr(self) +@dataclass +class StatusV2(RoborockBase): + """ + This is a new version of the Status object. + This is the result of GET_STATUS from the api. + """ + + msg_ver: int | None = None + msg_seq: int | None = None + state: RoborockStateCode | None = None + battery: int | None = None + clean_time: int | None = None + clean_area: int | None = None + error_code: RoborockErrorCode | None = None + map_present: int | None = None + in_cleaning: RoborockInCleaning | None = None + in_returning: int | None = None + in_fresh_state: int | None = None + lab_status: int | None = None + water_box_status: int | None = None + back_type: int | None = None + wash_phase: int | None = None + wash_ready: int | None = None + fan_power: int | None = None + dnd_enabled: int | None = None + map_status: int | None = None + is_locating: int | None = None + lock_status: int | None = None + water_box_mode: int | None = None + water_box_carriage_status: int | None = None + mop_forbidden_enable: int | None = None + camera_status: int | None = None + is_exploring: int | None = None + home_sec_status: int | None = None + home_sec_enable_password: int | None = None + adbumper_status: list[int] | None = None + water_shortage_status: int | None = None + dock_type: RoborockDockTypeCode | None = None + dust_collection_status: int | None = None + auto_dust_collection: int | None = None + avoid_count: int | None = None + mop_mode: int | None = None + debug_mode: int | None = None + collision_avoid_status: int | None = None + switch_map_mode: int | None = None + dock_error_status: RoborockDockErrorCode | None = None + charge_status: int | None = None + unsave_map_reason: int | None = None + unsave_map_flag: int | None = None + wash_status: int | None = None + distance_off: int | None = None + in_warmup: int | None = None + dry_status: int | None = None + rdt: int | None = None + clean_percent: int | None = None + rss: int | None = None + dss: int | None = None + common_status: int | None = None + corner_clean_mode: int | None = None + last_clean_t: int | None = None + replenish_mode: int | None = None + repeat: int | None = None + kct: int | None = None + subdivision_sets: int | None = None + + @property + def square_meter_clean_area(self) -> float | None: + return round(self.clean_area / 1000000, 1) if self.clean_area is not None else None + + @property + def error_code_name(self) -> str | None: + return self.error_code.name if self.error_code is not None else None + + @property + def state_name(self) -> str | None: + return self.state.name if self.state is not None else None + + @property + def current_map(self) -> int | None: + """Returns the current map ID if the map is present.""" + if self.map_status is not None: + map_flag = self.map_status >> 2 + if map_flag != NO_MAP: + return map_flag + return None + + @property + def clear_water_box_status(self) -> ClearWaterBoxStatus | None: + if self.dss: + return ClearWaterBoxStatus((self.dss >> 2) & 3) + return None + + @property + def dirty_water_box_status(self) -> DirtyWaterBoxStatus | None: + if self.dss: + return DirtyWaterBoxStatus((self.dss >> 4) & 3) + return None + + @property + def dust_bag_status(self) -> DustBagStatus | None: + if self.dss: + return DustBagStatus((self.dss >> 6) & 3) + return None + + @property + def water_box_filter_status(self) -> int | None: + if self.dss: + return (self.dss >> 8) & 3 + return None + + @property + def clean_fluid_status(self) -> int | None: + if self.dss: + return (self.dss >> 10) & 3 + return None + + @property + def hatch_door_status(self) -> int | None: + if self.dss: + return (self.dss >> 12) & 7 + return None + + @property + def dock_cool_fan_status(self) -> int | None: + if self.dss: + return (self.dss >> 15) & 3 + return None + + def __repr__(self) -> str: + return _attr_repr(self) + + @dataclass class S4MaxStatus(Status): fan_power: RoborockFanSpeedS6Pure | None = None diff --git a/roborock/devices/device_manager.py b/roborock/devices/device_manager.py index 1fb5ec40..7be6208b 100644 --- a/roborock/devices/device_manager.py +++ b/roborock/devices/device_manager.py @@ -215,6 +215,7 @@ def device_creator(home_data: HomeData, device: HomeDataDevice, product: HomeDat web_api, device_cache=device_cache, map_parser_config=map_parser_config, + region=user_data.region, ) case DeviceVersion.A01: channel = create_mqtt_channel(user_data, mqtt_params, mqtt_session, device) diff --git a/roborock/devices/traits/v1/__init__.py b/roborock/devices/traits/v1/__init__.py index eb269838..fc9e4753 100644 --- a/roborock/devices/traits/v1/__init__.py +++ b/roborock/devices/traits/v1/__init__.py @@ -48,7 +48,6 @@ import logging from dataclasses import dataclass, field, fields -from functools import cache from typing import Any, get_args from roborock.data.containers import HomeData, HomeDataProduct, RoborockBase @@ -174,6 +173,7 @@ def __init__( web_api: UserWebApiClient, device_cache: DeviceCache, map_parser_config: MapParserConfig | None = None, + region: str | None = None, ) -> None: """Initialize the V1TraitProps.""" self._device_uid = device_uid @@ -182,14 +182,15 @@ def __init__( self._map_rpc_channel = map_rpc_channel self._web_api = web_api self._device_cache = device_cache + self._region = region - self.status = StatusTrait(product) + self.device_features = DeviceFeaturesTrait(product.product_nickname, self._device_cache) + self.status = StatusTrait(self.device_features, region=self._region) self.consumables = ConsumableTrait() self.rooms = RoomsTrait(home_data) self.maps = MapsTrait(self.status) self.map_content = MapContentTrait(map_parser_config) self.home = HomeTrait(self.status, self.maps, self.map_content, self.rooms, self._device_cache) - self.device_features = DeviceFeaturesTrait(product.product_nickname, self._device_cache) self.network_info = NetworkInfoTrait(device_uid, self._device_cache) self.routines = RoutinesTrait(device_uid, web_api) @@ -200,6 +201,8 @@ def __init__( if (union_args := get_args(item.type)) is None or len(union_args) > 0: continue _LOGGER.debug("Trait '%s' is supported, initializing", item.name) + if not callable(item.type): + continue trait = item.type() setattr(self, item.name, trait) # This is a hack to allow setting the rpc_channel on all traits. This is @@ -318,6 +321,7 @@ def create( web_api: UserWebApiClient, device_cache: DeviceCache, map_parser_config: MapParserConfig | None = None, + region: str | None = None, ) -> PropertiesApi: """Create traits for V1 devices.""" return PropertiesApi( @@ -330,4 +334,5 @@ def create( web_api, device_cache, map_parser_config, + region=region, ) diff --git a/roborock/devices/traits/v1/status.py b/roborock/devices/traits/v1/status.py index 08cd0c45..41c9e567 100644 --- a/roborock/devices/traits/v1/status.py +++ b/roborock/devices/traits/v1/status.py @@ -1,24 +1,70 @@ +from functools import cached_property from typing import Self -from roborock.data import HomeDataProduct, ModelStatus, S7MaxVStatus, Status -from roborock.devices.traits.v1 import common +from roborock import CleanRoutes, StatusV2, VacuumModes, WaterModes, get_clean_modes, get_clean_routes, get_water_modes from roborock.roborock_typing import RoborockCommand +from . import common +from .device_features import DeviceFeaturesTrait -class StatusTrait(Status, common.V1TraitMixin): + +class StatusTrait(StatusV2, common.V1TraitMixin): """Trait for managing the status of Roborock devices.""" command = RoborockCommand.GET_STATUS - def __init__(self, product_info: HomeDataProduct) -> None: + def __init__(self, device_feature_trait: DeviceFeaturesTrait, region: str | None = None) -> None: """Initialize the StatusTrait.""" - self._product_info = product_info + super().__init__() + self._device_features_trait = device_feature_trait + self._region = region + + @cached_property + def fan_speed_options(self) -> list[VacuumModes]: + return get_clean_modes(self._device_features_trait) + + @cached_property + def fan_speed_mapping(self) -> dict[int, str]: + return {fan.code: fan.name for fan in self.fan_speed_options} + + @cached_property + def water_mode_options(self) -> list[WaterModes]: + return get_water_modes(self._device_features_trait) + + @cached_property + def water_mode_mapping(self) -> dict[int, str]: + return {mop.code: mop.name for mop in self.water_mode_options} + + @cached_property + def mop_route_options(self) -> list[CleanRoutes]: + return get_clean_routes(self._device_features_trait, self._region or "us") + + @cached_property + def mop_route_mapping(self) -> dict[int, str]: + return {route.code: route.name for route in self.mop_route_options} + + @property + def fan_speed_name(self) -> str | None: + if self.fan_power is None: + return None + return self.fan_speed_mapping.get(self.fan_power) + + @property + def water_mode_name(self) -> str | None: + if self.water_box_mode is None: + return None + return self.water_mode_mapping.get(self.water_box_mode) + + @property + def mop_route_name(self) -> str | None: + if self.mop_mode is None: + return None + return self.mop_route_mapping.get(self.mop_mode) def _parse_response(self, response: common.V1ResponseData) -> Self: """Parse the response from the device into a CleanSummary.""" - status_type: type[Status] = ModelStatus.get(self._product_info.model, S7MaxVStatus) if isinstance(response, list): response = response[0] if isinstance(response, dict): - return status_type.from_dict(response) + return StatusV2.from_dict(response) raise ValueError(f"Unexpected status format: {response!r}") diff --git a/tests/devices/__snapshots__/test_v1_device.ambr b/tests/devices/__snapshots__/test_v1_device.ambr index dedcddca..b4f58fc6 100644 --- a/tests/devices/__snapshots__/test_v1_device.ambr +++ b/tests/devices/__snapshots__/test_v1_device.ambr @@ -832,7 +832,7 @@ }) # --- # name: test_device_trait_command_parsing[status] - StatusTrait(adbumper_status=None, auto_dust_collection=None, avoid_count=None, back_type=None, battery=100, camera_status=None, charge_status=None, clean_area=91287500, clean_fluid_status=None, clean_percent=None, clean_time=5405, clear_water_box_status=None, collision_avoid_status=None, command=, common_status=None, corner_clean_mode=None, current_map=0, debug_mode=None, dirty_water_box_status=None, distance_off=0, dnd_enabled=1, dock_cool_fan_status=None, dock_error_status=None, dock_type=None, dry_status=None, dss=None, dust_bag_status=None, dust_collection_status=None, error_code=, error_code_name='none', fan_power=, fan_power_name='custom', fan_power_options=['off', 'quiet', 'balanced', 'turbo', 'max', 'custom', 'max_plus'], hatch_door_status=None, home_sec_enable_password=None, home_sec_status=None, in_cleaning=, in_fresh_state=1, in_returning=0, in_warmup=None, is_exploring=None, is_locating=0, kct=None, lab_status=1, last_clean_t=None, lock_status=0, map_present=1, map_status=3, mop_forbidden_enable=0, mop_mode=None, mop_mode_name=None, msg_seq=515, msg_ver=2, rdt=None, repeat=None, replenish_mode=None, rss=None, square_meter_clean_area=91.3, state=, state_name='charging', subdivision_sets=None, switch_map_mode=None, unsave_map_flag=0, unsave_map_reason=4, wash_phase=None, wash_ready=None, wash_status=None, water_box_carriage_status=0, water_box_filter_status=None, water_box_mode=, water_box_mode_name='custom', water_box_status=0, water_shortage_status=None) + StatusTrait(adbumper_status=None, auto_dust_collection=None, avoid_count=None, back_type=None, battery=100, camera_status=None, charge_status=None, clean_area=91287500, clean_fluid_status=None, clean_percent=None, clean_time=5405, clear_water_box_status=None, collision_avoid_status=None, command=, common_status=None, corner_clean_mode=None, current_map=0, debug_mode=None, dirty_water_box_status=None, distance_off=0, dnd_enabled=1, dock_cool_fan_status=None, dock_error_status=None, dock_type=None, dry_status=None, dss=None, dust_bag_status=None, dust_collection_status=None, error_code=, error_code_name='none', fan_power=106, fan_speed_mapping={101: 'QUIET', 102: 'BALANCED', 103: 'TURBO', 104: 'MAX', 105: 'GENTLE'}, fan_speed_name=None, fan_speed_options=[, , , , ], hatch_door_status=None, home_sec_enable_password=None, home_sec_status=None, in_cleaning=, in_fresh_state=1, in_returning=0, in_warmup=None, is_exploring=None, is_locating=0, kct=None, lab_status=1, last_clean_t=None, lock_status=0, map_present=1, map_status=3, mop_forbidden_enable=0, mop_mode=None, mop_route_mapping={300: 'STANDARD', 301: 'DEEP'}, mop_route_name=None, mop_route_options=[, ], msg_seq=515, msg_ver=2, rdt=None, repeat=None, replenish_mode=None, rss=None, square_meter_clean_area=91.3, state=, state_name='charging', subdivision_sets=None, switch_map_mode=None, unsave_map_flag=0, unsave_map_reason=4, wash_phase=None, wash_ready=None, wash_status=None, water_box_carriage_status=0, water_box_filter_status=None, water_box_mode=204, water_box_status=0, water_mode_mapping={200: 'OFF', 201: 'LOW', 202: 'MEDIUM', 203: 'HIGH'}, water_mode_name=None, water_mode_options=[, , , ], water_shortage_status=None) # --- # name: test_device_trait_command_parsing[status].1 dict({ diff --git a/tests/devices/test_v1_device.py b/tests/devices/test_v1_device.py index 3762d9a5..94c85298 100644 --- a/tests/devices/test_v1_device.py +++ b/tests/devices/test_v1_device.py @@ -65,6 +65,7 @@ def device_fixture(channel: AsyncMock, rpc_channel: AsyncMock, mqtt_rpc_channel: AsyncMock(), AsyncMock(), device_cache=DeviceCache(HOME_DATA.devices[0].duid, NoCache()), + region=USER_DATA.region, ), ) diff --git a/tests/devices/traits/v1/fixtures.py b/tests/devices/traits/v1/fixtures.py index 78f8e3d8..8abbf0eb 100644 --- a/tests/devices/traits/v1/fixtures.py +++ b/tests/devices/traits/v1/fixtures.py @@ -81,6 +81,7 @@ def device_fixture( mock_map_rpc_channel, web_api_client, device_cache=device_cache, + region=USER_DATA.region, ), )