Skip to content

Commit 53b2676

Browse files
committed
feat: add dynamic changing status
1 parent 279283d commit 53b2676

File tree

8 files changed

+126
-86
lines changed

8 files changed

+126
-86
lines changed

roborock/containers.py

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@
1010
from functools import cached_property
1111
from typing import Any, NamedTuple, get_args, get_origin
1212

13+
from .clean_modes import (
14+
CleanRoutes,
15+
VacuumModes,
16+
VacuumModesOld,
17+
WaterModes,
18+
get_clean_modes,
19+
get_clean_routes,
20+
get_water_modes,
21+
)
1322
from .code_mappings import (
1423
SHORT_MODEL_TO_ENUM,
1524
RoborockCategory,
@@ -19,7 +28,6 @@
1928
RoborockDockTypeCode,
2029
RoborockDockWashTowelModeCode,
2130
RoborockErrorCode,
22-
RoborockFanPowerCode,
2331
RoborockFanSpeedP10,
2432
RoborockFanSpeedQ7Max,
2533
RoborockFanSpeedQRevoCurv,
@@ -34,7 +42,6 @@
3442
RoborockFinishReason,
3543
RoborockInCleaning,
3644
RoborockModeEnum,
37-
RoborockMopIntensityCode,
3845
RoborockMopIntensityP10,
3946
RoborockMopIntensityQ7Max,
4047
RoborockMopIntensityQRevoCurv,
@@ -46,7 +53,6 @@
4653
RoborockMopIntensityS8MaxVUltra,
4754
RoborockMopIntensitySaros10,
4855
RoborockMopIntensitySaros10R,
49-
RoborockMopModeCode,
5056
RoborockMopModeQRevoCurv,
5157
RoborockMopModeQRevoMaster,
5258
RoborockMopModeQRevoMaxV,
@@ -92,7 +98,6 @@
9298
ROBOROCK_G20S_Ultra,
9399
)
94100
from .device_features import DeviceFeatures
95-
from .exceptions import RoborockException
96101

97102
_LOGGER = logging.getLogger(__name__)
98103

@@ -357,12 +362,12 @@ class Status(RoborockBase):
357362
back_type: int | None = None
358363
wash_phase: int | None = None
359364
wash_ready: int | None = None
360-
fan_power: RoborockFanPowerCode | None = None
365+
fan_power: int | None = None
361366
dnd_enabled: int | None = None
362367
map_status: int | None = None
363368
is_locating: int | None = None
364369
lock_status: int | None = None
365-
water_box_mode: RoborockMopIntensityCode | None = None
370+
water_box_mode: int | None = None
366371
water_box_carriage_status: int | None = None
367372
mop_forbidden_enable: int | None = None
368373
camera_status: int | None = None
@@ -375,7 +380,7 @@ class Status(RoborockBase):
375380
dust_collection_status: int | None = None
376381
auto_dust_collection: int | None = None
377382
avoid_count: int | None = None
378-
mop_mode: RoborockMopModeCode | None = None
383+
mop_mode: int | None = None
379384
debug_mode: int | None = None
380385
collision_avoid_status: int | None = None
381386
switch_map_mode: int | None = None
@@ -406,28 +411,6 @@ def __post_init__(self) -> None:
406411
self.error_code_name = self.error_code.name
407412
if self.state is not None:
408413
self.state_name = self.state.name
409-
if self.water_box_mode is not None:
410-
self.water_box_mode_name = self.water_box_mode.name
411-
if self.fan_power is not None:
412-
self.fan_power_options = self.fan_power.keys()
413-
self.fan_power_name = self.fan_power.name
414-
if self.mop_mode is not None:
415-
self.mop_mode_name = self.mop_mode.name
416-
417-
def get_fan_speed_code(self, fan_speed: str) -> int:
418-
if self.fan_power is None:
419-
raise RoborockException("Attempted to get fan speed before status has been updated.")
420-
return self.fan_power.as_dict().get(fan_speed)
421-
422-
def get_mop_intensity_code(self, mop_intensity: str) -> int:
423-
if self.water_box_mode is None:
424-
raise RoborockException("Attempted to get mop_intensity before status has been updated.")
425-
return self.water_box_mode.as_dict().get(mop_intensity)
426-
427-
def get_mop_mode_code(self, mop_mode: str) -> int:
428-
if self.mop_mode is None:
429-
raise RoborockException("Attempted to get mop_mode before status has been updated.")
430-
return self.mop_mode.as_dict().get(mop_mode)
431414

432415
@property
433416
def current_map(self) -> int | None:
@@ -574,6 +557,34 @@ class Saros10Status(Status):
574557
}
575558

576559

560+
def get_custom_status(features: DeviceFeatures, region: str) -> "DeviceStatus":
561+
_available_fan_speeds = get_clean_modes(features)
562+
_available_fan_speed_mapping = {fan.code: fan.name for fan in _available_fan_speeds}
563+
_available_water_modes = get_water_modes(features)
564+
_available_water_modes_mapping = {mop.code: mop.name for mop in _available_water_modes}
565+
_available_mop_routes = get_clean_routes(features, region)
566+
_available_mop_routes_mapping = {route.code: route.name for route in _available_mop_routes}
567+
568+
class DeviceStatus(Status):
569+
available_fan_speeds: list[VacuumModes] | list[VacuumModesOld] = _available_fan_speeds
570+
available_water_modes: list[WaterModes] = _available_water_modes
571+
available_mop_routes: list[CleanRoutes] = _available_mop_routes
572+
573+
@property
574+
def fan_speed(self) -> VacuumModes | None:
575+
return _available_fan_speed_mapping.get(self.fan_power)
576+
577+
@property
578+
def water_mode(self) -> WaterModes | None:
579+
return _available_water_modes_mapping.get(self.water_box_mode)
580+
581+
@property
582+
def mop_route(self) -> CleanRoutes | None:
583+
return _available_mop_routes_mapping.get(self.mop_mode)
584+
585+
return DeviceStatus
586+
587+
577588
@dataclass
578589
class DnDTimer(RoborockBaseTimer):
579590
"""DnDTimer"""
@@ -762,6 +773,7 @@ class DeviceData(RoborockBase):
762773
host: str | None = None
763774
product_nickname: RoborockProductNickname | None = None
764775
device_features: DeviceFeatures | None = None
776+
region: str | None = None
765777

766778
def __post_init__(self):
767779
self.product_nickname = SHORT_MODEL_TO_ENUM.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS)

roborock/devices/traits/v1/status.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Self
22

3-
from roborock.containers import HomeDataProduct, ModelStatus, S7MaxVStatus, Status
3+
from roborock.containers import HomeDataProduct, Status, get_custom_status
44
from roborock.devices.traits.v1 import common
55
from roborock.roborock_typing import RoborockCommand
66

@@ -13,12 +13,12 @@ class StatusTrait(Status, common.V1TraitMixin):
1313
def __init__(self, product_info: HomeDataProduct) -> None:
1414
"""Initialize the StatusTrait."""
1515
self._product_info = product_info
16+
self._status_type = get_custom_status(self.device_info.device_features, self.device_info.region)
1617

1718
def _parse_response(self, response: common.V1ResponseData) -> Self:
1819
"""Parse the response from the device into a CleanSummary."""
19-
status_type: type[Status] = ModelStatus.get(self._product_info.model, S7MaxVStatus)
2020
if isinstance(response, list):
2121
response = response[0]
2222
if isinstance(response, dict):
23-
return status_type.from_dict(response)
23+
return self._status_type.from_dict(response)
2424
raise ValueError(f"Unexpected status format: {response!r}")

roborock/version_1_apis/roborock_client_v1.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
import time
55
from abc import ABC, abstractmethod
66
from collections.abc import Callable, Coroutine
7+
from functools import cached_property
78
from typing import Any, TypeVar, final
89

910
from roborock import (
1011
AppInitStatus,
12+
DeviceFeatures,
1113
DeviceProp,
1214
DockSummary,
1315
RoborockCommand,
@@ -33,17 +35,16 @@
3335
DnDTimer,
3436
DustCollectionMode,
3537
FlowLedStatus,
36-
ModelStatus,
3738
MultiMapsList,
3839
NetworkInfo,
3940
RoborockBase,
4041
RoomMapping,
41-
S7MaxVStatus,
4242
ServerTimer,
4343
SmartWashParams,
4444
Status,
4545
ValleyElectricityTimer,
4646
WashTowelMode,
47+
get_custom_status,
4748
)
4849
from roborock.protocols.v1_protocol import MapResponse, SecurityData, create_map_response_decoder
4950
from roborock.roborock_message import (
@@ -159,7 +160,7 @@ def __init__(self, device_info: DeviceData, security_data: SecurityData | None)
159160
self._diagnostic_data.update({"misc_info": security_data.to_diagnostic_data()})
160161
self._map_response_decoder = create_map_response_decoder(security_data)
161162

162-
self._status_type: type[Status] = ModelStatus.get(device_info.model, S7MaxVStatus)
163+
self._status_type: Status | None = None
163164
self.cache: dict[CacheableAttribute, AttributeCache] = {
164165
cacheable_attribute: AttributeCache(attr, self._send_command)
165166
for cacheable_attribute, attr in get_cache_map().items()
@@ -172,15 +173,17 @@ async def async_release(self) -> None:
172173
await super().async_release()
173174
[item.stop() for item in self.cache.values()]
174175

175-
@property
176+
@cached_property
176177
def status_type(self) -> type[Status]:
177178
"""Gets the status type for this device"""
178-
return self._status_type
179+
if self.device_info.device_features is None or self.device_info.region is None:
180+
raise RoborockException("Device features and region are required to get status type")
181+
return get_custom_status(self.device_info.device_features, self.device_info.region)
179182

180183
async def get_status(self) -> Status:
181-
data = self._status_type.from_dict(await self.cache[CacheableAttribute.status].async_value(force=True))
184+
data = self.status_type.from_dict(await self.cache[CacheableAttribute.status].async_value(force=True))
182185
if data is None:
183-
return self._status_type()
186+
return self.status_type()
184187
return data
185188

186189
async def get_dnd_timer(self) -> DnDTimer | None:
@@ -345,7 +348,14 @@ async def load_multi_map(self, map_flag: int) -> None:
345348

346349
async def get_app_init_status(self) -> AppInitStatus:
347350
"""Gets the app init status (needed for determining vacuum capabilities)."""
348-
return await self.send_command(RoborockCommand.APP_GET_INIT_STATUS, return_type=AppInitStatus)
351+
init_status = await self.send_command(RoborockCommand.APP_GET_INIT_STATUS, return_type=AppInitStatus)
352+
self.device_info.device_features = DeviceFeatures.from_feature_flags(
353+
init_status.new_feature_info,
354+
init_status.new_feature_info_str,
355+
init_status.feature_info,
356+
self.device_info.product_nickname,
357+
)
358+
return init_status
349359

350360
@abstractmethod
351361
async def _send_command(

roborock/web_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,7 @@ async def get_products(self, user_data: UserData) -> ProductResponse:
552552
product_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid})
553553
product_response = await product_request.request(
554554
"get",
555-
"/api/v4/product",
555+
"/api/v5/product",
556556
headers={"Authorization": user_data.token},
557557
)
558558
if product_response is None:
@@ -568,7 +568,7 @@ async def download_code(self, user_data: UserData, product_id: int):
568568
base_url = await self._get_base_url()
569569
header_clientid = self._get_header_client_id()
570570
product_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid})
571-
request = {"apilevel": 99999, "productids": [product_id], "type": 2}
571+
request = {"apilevel": 10040, "productids": [product_id], "type": 2}
572572
response = await product_request.request(
573573
"post",
574574
"/api/v1/appplugin",

tests/conftest.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import pytest
1212
from aioresponses import aioresponses
1313

14-
from roborock import HomeData, UserData
14+
from roborock import SHORT_MODEL_TO_ENUM, DeviceFeatures, HomeData, UserData
1515
from roborock.containers import DeviceData
1616
from roborock.roborock_message import RoborockMessage
1717
from roborock.version_1_apis.roborock_local_client_v1 import RoborockLocalClientV1
@@ -389,3 +389,27 @@ async def _subscribe(self, callback: Callable[[RoborockMessage], None]) -> Calla
389389
"""Simulate subscribing to messages."""
390390
self.subscribers.append(callback)
391391
return lambda: self.subscribers.remove(callback)
392+
393+
394+
@pytest.fixture(name="s7_device_features")
395+
def s7_device_features_fixture() -> DeviceFeatures:
396+
model = "roborock.vacuum.a15"
397+
product_nickname = SHORT_MODEL_TO_ENUM.get(model.split(".")[-1])
398+
return DeviceFeatures.from_feature_flags(
399+
new_feature_info=636084721975295,
400+
new_feature_info_str="0000000000002000",
401+
feature_info=[111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 122, 123, 124, 125],
402+
product_nickname=product_nickname,
403+
)
404+
405+
406+
@pytest.fixture(name="qrevo_maxv_device_features")
407+
def qrevo_maxv_device_features_fixture() -> DeviceFeatures:
408+
model = "roborock.vacuum.a87"
409+
product_nickname = SHORT_MODEL_TO_ENUM.get(model.split(".")[-1])
410+
return DeviceFeatures.from_feature_flags(
411+
new_feature_info=4499197267967999,
412+
new_feature_info_str="508A977F7EFEFFFF",
413+
feature_info=[111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125],
414+
product_nickname=product_nickname,
415+
)

tests/devices/test_v1_device.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import pytest
88
from syrupy import SnapshotAssertion
99

10-
from roborock.containers import HomeData, S7MaxVStatus, UserData
10+
from roborock.containers import HomeData, UserData
1111
from roborock.devices.device import RoborockDevice
1212
from roborock.devices.traits import v1
1313
from roborock.devices.traits.v1.common import V1TraitMixin
@@ -18,7 +18,6 @@
1818

1919
USER_DATA = UserData.from_dict(mock_data.USER_DATA)
2020
HOME_DATA = HomeData.from_dict(mock_data.HOME_DATA_RAW)
21-
STATUS = S7MaxVStatus.from_dict(mock_data.STATUS)
2221

2322
TESTDATA = pathlib.Path("tests/protocols/testdata/v1_protocol/")
2423

0 commit comments

Comments
 (0)