Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion roborock/data/v1/v1_clean_modes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
134 changes: 134 additions & 0 deletions roborock/data/v1/v1_containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions roborock/devices/device_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 8 additions & 3 deletions roborock/devices/traits/v1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -330,4 +334,5 @@ def create(
web_api,
device_cache,
map_parser_config,
region=region,
)
60 changes: 53 additions & 7 deletions roborock/devices/traits/v1/status.py
Original file line number Diff line number Diff line change
@@ -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}")
2 changes: 1 addition & 1 deletion tests/devices/__snapshots__/test_v1_device.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -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=<RoborockCommand.GET_STATUS: 'get_status'>, 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=<RoborockErrorCode.none: 0>, error_code_name='none', fan_power=<RoborockFanSpeedS7MaxV.custom: 106>, 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=<RoborockInCleaning.complete: 0>, 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=<RoborockStateCode.charging: 8>, 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=<RoborockMopIntensityS7.custom: 204>, 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=<RoborockCommand.GET_STATUS: 'get_status'>, 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=<RoborockErrorCode.none: 0>, 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=[<VacuumModes.QUIET: 'quiet'>, <VacuumModes.BALANCED: 'balanced'>, <VacuumModes.TURBO: 'turbo'>, <VacuumModes.MAX: 'max'>, <VacuumModes.GENTLE: 'gentle'>], hatch_door_status=None, home_sec_enable_password=None, home_sec_status=None, in_cleaning=<RoborockInCleaning.complete: 0>, 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=[<CleanRoutes.STANDARD: 'standard'>, <CleanRoutes.DEEP: 'deep'>], msg_seq=515, msg_ver=2, rdt=None, repeat=None, replenish_mode=None, rss=None, square_meter_clean_area=91.3, state=<RoborockStateCode.charging: 8>, 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=[<WaterModes.OFF: 'off'>, <WaterModes.LOW: 'low'>, <WaterModes.MEDIUM: 'medium'>, <WaterModes.HIGH: 'high'>], water_shortage_status=None)
# ---
# name: test_device_trait_command_parsing[status].1
dict({
Expand Down
1 change: 1 addition & 0 deletions tests/devices/test_v1_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
)

Expand Down
1 change: 1 addition & 0 deletions tests/devices/traits/v1/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def device_fixture(
mock_map_rpc_channel,
web_api_client,
device_cache=device_cache,
region=USER_DATA.region,
),
)

Expand Down
Loading