Skip to content

Commit 24e34c0

Browse files
committed
chore: move device_features to seperate file and add some tests and rework device_features
1 parent 01ec906 commit 24e34c0

File tree

4 files changed

+416
-243
lines changed

4 files changed

+416
-243
lines changed

roborock/code_mappings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ class RoborockProductNickname(Enum):
110110
VIVIANC = ProductInfo(nickname="VivianC", short_models=("a158", "a159"))
111111

112112

113-
short_model_to_enum = {model: product for product in RoborockProductNickname for model in product.value.short_models}
113+
SHORT_MODEL_TO_ENUM = {model: product for product in RoborockProductNickname for model in product.value.short_models}
114114

115115

116116
class RoborockStateCode(RoborockEnum):

roborock/containers.py

Lines changed: 5 additions & 242 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
import json
55
import logging
66
import re
7-
from dataclasses import asdict, dataclass, field, fields
7+
from dataclasses import asdict, dataclass, field
88
from datetime import timezone
9-
from enum import Enum, IntEnum
9+
from enum import Enum
1010
from functools import cached_property
1111
from typing import Any, NamedTuple, get_args, get_origin
1212

1313
from .code_mappings import (
14+
SHORT_MODEL_TO_ENUM,
1415
RoborockCategory,
1516
RoborockCleanType,
1617
RoborockDockDustCollectionModeCode,
@@ -56,7 +57,6 @@
5657
RoborockProductNickname,
5758
RoborockStartType,
5859
RoborockStateCode,
59-
short_model_to_enum,
6060
)
6161
from .const import (
6262
CLEANING_BRUSH_REPLACE_TIME,
@@ -89,6 +89,7 @@
8989
STRAINER_REPLACE_TIME,
9090
ROBOROCK_G20S_Ultra,
9191
)
92+
from .device_features import DeviceFeatures
9293
from .exceptions import RoborockException
9394

9495
_LOGGER = logging.getLogger(__name__)
@@ -310,238 +311,6 @@ class HomeDataDevice(RoborockBase):
310311
f: bool | None = None
311312

312313

313-
class NewFeatureStrBit(IntEnum):
314-
TWO_KEY_REAL_TIME_VIDEO = 32
315-
TWO_KEY_RTV_IN_CHARGING = 33
316-
DIRTY_REPLENISH_CLEAN = 34
317-
AUTO_DELIVERY_FIELD_IN_GLOBAL_STATUS = 35
318-
AVOID_COLLISION_MODE = 36
319-
VOICE_CONTROL = 37
320-
NEW_ENDPOINT = 38
321-
PUMPING_WATER = 39
322-
CORNER_MOP_STRECH = 40
323-
HOT_WASH_TOWEL = 41
324-
FLOOR_DIR_CLEAN_ANY_TIME = 42
325-
PET_SUPPLIES_DEEP_CLEAN = 43
326-
MOP_SHAKE_WATER_MAX = 45
327-
EXACT_CUSTOM_MODE = 47
328-
CARPET_CUSTOM_CLEAN = 49
329-
PET_SNAPSHOT = 50
330-
CUSTOM_CLEAN_MODE_COUNT = 51
331-
NEW_AI_RECOGNITION = 52
332-
AUTO_COLLECTION_2 = 53
333-
RIGHT_BRUSH_STRETCH = 54
334-
SMART_CLEAN_MODE_SET = 55
335-
DIRTY_OBJECT_DETECT = 56
336-
NO_NEED_CARPET_PRESS_SET = 57
337-
VOICE_CONTROL_LED = 58
338-
WATER_LEAK_CHECK = 60
339-
MIN_BATTERY_15_TO_CLEAN_TASK = 62
340-
GAP_DEEP_CLEAN = 63
341-
OBJECT_DETECT_CHECK = 64
342-
IDENTIFY_ROOM = 66
343-
MATTER = 67
344-
WORKDAY_HOLIDAY = 69
345-
CLEAN_DIRECT_STATUS = 70
346-
MAP_ERASER = 71
347-
OPTIMIZE_BATTERY = 72
348-
ACTIVATE_VIDEO_CHARGING_AND_STANDBY = 73
349-
CARPET_LONG_HAIRED = 75
350-
CLEAN_HISTORY_TIME_LINE = 76
351-
MAX_ZONE_OPENED = 77
352-
EXHIBITION_FUNCTION = 78
353-
LDS_LIFTING = 79
354-
AUTO_TEAR_DOWN_MOP = 80
355-
SAMLL_SIDE_MOP = 81
356-
SUPPORT_SIDE_BRUSH_UP_DOWN = 82
357-
DRY_INTERVAL_TIMER = 83
358-
UVC_STERILIZE = 84
359-
MIDWAY_BACK_TO_DOCK = 85
360-
SUPPORT_MAIN_BRUSH_UP_DOWN = 86
361-
EGG_DANCE_MODE = 87
362-
363-
364-
@dataclass
365-
class DeviceFeatures(RoborockBase):
366-
"""Represents the features supported by a Roborock device."""
367-
368-
# Features derived from robot_new_features
369-
is_show_clean_finish_reason_supported: bool = field(metadata={"robot_new_features": 1})
370-
is_resegment_supported: bool = field(metadata={"robot_new_features": 4})
371-
is_video_monitor_supported: bool = field(metadata={"robot_new_features": 8})
372-
is_any_state_transit_goto_supported: bool = field(metadata={"robot_new_features": 16})
373-
is_fw_filter_obstacle_supported: bool = field(metadata={"robot_new_features": 32})
374-
is_video_settings_supported: bool = field(metadata={"robot_new_features": 64})
375-
is_ignore_unknown_map_object_supported: bool = field(metadata={"robot_new_features": 128})
376-
is_set_child_supported: bool = field(metadata={"robot_new_features": 256})
377-
is_carpet_supported: bool = field(metadata={"robot_new_features": 512})
378-
is_record_allowed: bool = field(metadata={"robot_new_features": 1024})
379-
is_mop_path_supported: bool = field(metadata={"robot_new_features": 2048})
380-
is_current_map_restore_enabled: bool = field(metadata={"robot_new_features": 8192})
381-
is_room_name_supported: bool = field(metadata={"robot_new_features": 16384})
382-
is_photo_upload_supported: bool = field(metadata={"robot_new_features": 65536})
383-
is_shake_mop_set_supported: bool = field(metadata={"robot_new_features": 262144})
384-
is_map_beautify_internal_debug_supported: bool = field(metadata={"robot_new_features": 2097152})
385-
is_new_data_for_clean_history_supported: bool = field(metadata={"robot_new_features": 4194304})
386-
is_new_data_for_clean_history_detail_supported: bool = field(metadata={"robot_new_features": 8388608})
387-
is_flow_led_setting_supported: bool = field(metadata={"robot_new_features": 16777216})
388-
is_dust_collection_setting_supported: bool = field(metadata={"robot_new_features": 33554432})
389-
is_rpc_retry_supported: bool = field(metadata={"robot_new_features": 67108864})
390-
is_avoid_collision_supported: bool = field(metadata={"robot_new_features": 134217728})
391-
is_support_set_switch_map_mode_supported: bool = field(metadata={"robot_new_features": 268435456})
392-
is_map_carpet_add_support: bool = field(metadata={"robot_new_features": 1073741824})
393-
is_custom_water_box_distance_supported: bool = field(metadata={"robot_new_features": 2147483648})
394-
395-
# Features derived from unhexed_feature_info
396-
is_support_smart_scene_supported: bool = field(metadata={"upper_32_bits": 1})
397-
is_support_floor_edit_supported: bool = field(metadata={"upper_32_bits": 3})
398-
is_support_furniture_supported: bool = field(metadata={"upper_32_bits": 4})
399-
is_wash_then_charge_cmd_supported: bool = field(metadata={"upper_32_bits": 5})
400-
is_support_room_tag_supported: bool = field(metadata={"upper_32_bits": 6})
401-
is_support_quick_map_builder_supported: bool = field(metadata={"upper_32_bits": 7})
402-
is_support_smart_global_clean_with_custom_mode_supported: bool = field(metadata={"upper_32_bits": 8})
403-
is_careful_slow_mop_supported: bool = field(metadata={"upper_32_bits": 9})
404-
is_egg_mode_supported: bool = field(metadata={"upper_32_bits": 10})
405-
is_carpet_show_on_map_supported: bool = field(metadata={"upper_32_bits": 12})
406-
is_supported_valley_electricity_supported: bool = field(metadata={"upper_32_bits": 13})
407-
is_unsave_map_reason_supported: bool = field(metadata={"upper_32_bits": 14})
408-
is_supported_download_test_voice_supported: bool = field(metadata={"upper_32_bits": 16})
409-
is_support_backup_map_supported: bool = field(metadata={"upper_32_bits": 17})
410-
is_support_custom_mode_in_cleaning_supported: bool = field(metadata={"upper_32_bits": 18})
411-
is_support_remote_control_in_call_supported: bool = field(metadata={"upper_32_bits": 19})
412-
413-
is_support_set_volume_in_call: bool = field(metadata={"unhexed_feature_info": 1})
414-
is_support_clean_estimate: bool = field(metadata={"unhexed_feature_info": 2})
415-
is_support_custom_dnd: bool = field(metadata={"unhexed_feature_info": 4})
416-
is_carpet_deep_clean_supported: bool = field(metadata={"unhexed_feature_info": 8})
417-
is_support_stuck_zone: bool = field(metadata={"unhexed_feature_info": 16})
418-
is_support_custom_door_sill: bool = field(metadata={"unhexed_feature_info": 32})
419-
is_wifi_manage_supported: bool = field(metadata={"unhexed_feature_info": 128})
420-
is_clean_route_fast_mode_supported: bool = field(metadata={"unhexed_feature_info": 256})
421-
is_support_cliff_zone: bool = field(metadata={"unhexed_feature_info": 512})
422-
is_support_smart_door_sill: bool = field(metadata={"unhexed_feature_info": 1024})
423-
is_support_floor_direction: bool = field(metadata={"unhexed_feature_info": 2048})
424-
is_back_charge_auto_wash_supported: bool = field(metadata={"unhexed_feature_info": 4096})
425-
is_super_deep_wash_supported: bool = field(metadata={"unhexed_feature_info": 32768})
426-
is_ces2022_supported: bool = field(metadata={"unhexed_feature_info": 65536})
427-
is_dss_believable_supported: bool = field(metadata={"unhexed_feature_info": 131072})
428-
is_main_brush_up_down_supported: bool = field(metadata={"unhexed_feature_info": 262144})
429-
is_goto_pure_clean_path_supported: bool = field(metadata={"unhexed_feature_info": 524288})
430-
is_water_up_down_drain_supported: bool = field(metadata={"unhexed_feature_info": 1048576})
431-
is_setting_carpet_first_supported: bool = field(metadata={"unhexed_feature_info": 8388608})
432-
is_clean_route_deep_slow_plus_supported: bool = field(metadata={"unhexed_feature_info": 16777216})
433-
is_left_water_drain_supported: bool = field(metadata={"unhexed_feature_info": 134217728})
434-
is_clean_count_setting_supported: bool = field(metadata={"unhexed_feature_info": 1073741824})
435-
is_corner_clean_mode_supported: bool = field(metadata={"unhexed_feature_info": 2147483648})
436-
437-
# --- Features from new_feature_info_str ---
438-
is_two_key_real_time_video_supported: bool = field(
439-
metadata={"new_feature_str_bit": NewFeatureStrBit.TWO_KEY_REAL_TIME_VIDEO}
440-
)
441-
is_two_key_rtv_in_charging_supported: bool = field(
442-
metadata={"new_feature_str_bit": NewFeatureStrBit.TWO_KEY_RTV_IN_CHARGING}
443-
)
444-
is_dirty_replenish_clean_supported: bool = field(
445-
metadata={"new_feature_str_bit": NewFeatureStrBit.DIRTY_REPLENISH_CLEAN}
446-
)
447-
is_avoid_collision_mode_str_supported: bool = field(
448-
metadata={"new_feature_str_bit": NewFeatureStrBit.AVOID_COLLISION_MODE}
449-
)
450-
is_voice_control_str_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.VOICE_CONTROL})
451-
is_new_endpoint_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.NEW_ENDPOINT})
452-
is_corner_mop_strech_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.CORNER_MOP_STRECH})
453-
is_hot_wash_towel_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.HOT_WASH_TOWEL})
454-
is_floor_dir_clean_any_time_supported: bool = field(
455-
metadata={"new_feature_str_bit": NewFeatureStrBit.FLOOR_DIR_CLEAN_ANY_TIME}
456-
)
457-
is_pet_supplies_deep_clean_supported: bool = field(
458-
metadata={"new_feature_str_bit": NewFeatureStrBit.PET_SUPPLIES_DEEP_CLEAN}
459-
)
460-
is_mop_shake_water_max_supported: bool = field(
461-
metadata={"new_feature_str_bit": NewFeatureStrBit.MOP_SHAKE_WATER_MAX}
462-
)
463-
is_exact_custom_mode_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.EXACT_CUSTOM_MODE})
464-
is_carpet_custom_clean_supported: bool = field(
465-
metadata={"new_feature_str_bit": NewFeatureStrBit.CARPET_CUSTOM_CLEAN}
466-
)
467-
is_pet_snapshot_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.PET_SNAPSHOT})
468-
is_custom_clean_mode_count_supported: bool = field(
469-
metadata={"new_feature_str_bit": NewFeatureStrBit.CUSTOM_CLEAN_MODE_COUNT}
470-
)
471-
is_new_ai_recognition_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.NEW_AI_RECOGNITION})
472-
is_auto_collection_2_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.AUTO_COLLECTION_2})
473-
is_right_brush_stretch_supported: bool = field(
474-
metadata={"new_feature_str_bit": NewFeatureStrBit.RIGHT_BRUSH_STRETCH}
475-
)
476-
is_smart_clean_mode_set_supported: bool = field(
477-
metadata={"new_feature_str_bit": NewFeatureStrBit.SMART_CLEAN_MODE_SET}
478-
)
479-
is_dirty_object_detect_supported: bool = field(
480-
metadata={"new_feature_str_bit": NewFeatureStrBit.DIRTY_OBJECT_DETECT}
481-
)
482-
is_no_need_carpet_press_set_supported: bool = field(
483-
metadata={"new_feature_str_bit": NewFeatureStrBit.NO_NEED_CARPET_PRESS_SET}
484-
)
485-
is_voice_control_led_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.VOICE_CONTROL_LED})
486-
is_water_leak_check_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.WATER_LEAK_CHECK})
487-
is_min_battery_15_to_clean_task_supported: bool = field(
488-
metadata={"new_feature_str_bit": NewFeatureStrBit.MIN_BATTERY_15_TO_CLEAN_TASK}
489-
)
490-
is_gap_deep_clean_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.GAP_DEEP_CLEAN})
491-
is_object_detect_check_supported: bool = field(
492-
metadata={"new_feature_str_bit": NewFeatureStrBit.OBJECT_DETECT_CHECK}
493-
)
494-
is_identify_room_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.IDENTIFY_ROOM})
495-
is_matter_supported: bool = field(metadata={"new_feature_str_bit": NewFeatureStrBit.MATTER})
496-
497-
# is_multi_map_segment_timer_supported: bool = field(default=False)
498-
# is_supported_drying_supported: bool = field(default=False)
499-
500-
@classmethod
501-
def _is_new_feature_str_support(cls, o: int, new_feature_info_str: str) -> bool:
502-
"""
503-
Checks feature 'o' in hex string 'new_feature_info_str'.
504-
"""
505-
try:
506-
l = o % 4
507-
target_index = -((o // 4) + 1)
508-
p = new_feature_info_str[target_index]
509-
hex_char_value = int(p, 16)
510-
is_set = (hex_char_value >> l) & 1
511-
return bool(is_set)
512-
except (IndexError, ValueError):
513-
return False
514-
515-
@classmethod
516-
def from_feature_flags(
517-
cls, robot_new_features: int, new_feature_set: str, product_nickname: RoborockProductNickname
518-
) -> DeviceFeatures:
519-
"""Creates a DeviceFeatures instance from raw feature flags."""
520-
unhexed_feature_info = int(new_feature_set[-8:], 16) if new_feature_set and len(new_feature_set) >= 8 else 0
521-
upper_32_bits = robot_new_features // (2**32)
522-
523-
kwargs: dict[str, Any] = {}
524-
525-
for f in fields(cls):
526-
if not f.metadata:
527-
continue
528-
529-
if "robot_new_features" in f.metadata:
530-
mask = f.metadata["robot_new_features"]
531-
kwargs[f.name] = bool(mask & robot_new_features)
532-
elif "upper_32_bits" in f.metadata:
533-
bit_index = f.metadata["upper_32_bits"]
534-
kwargs[f.name] = bool(robot_new_features and ((upper_32_bits >> bit_index) & 1))
535-
elif "unhexed_feature_info" in f.metadata:
536-
mask = f.metadata["unhexed_feature_info"]
537-
kwargs[f.name] = bool(mask & unhexed_feature_info)
538-
elif "new_feature_str_bit" in f.metadata:
539-
bit = f.metadata["new_feature_str_bit"]
540-
kwargs[f.name] = cls._is_new_feature_str_support(bit, new_feature_set)
541-
542-
return cls(**kwargs)
543-
544-
545314
@dataclass
546315
class HomeDataRoom(RoborockBase):
547316
id: int
@@ -990,13 +759,7 @@ class DeviceData(RoborockBase):
990759
device_features: DeviceFeatures | None = None
991760

992761
def __post_init__(self):
993-
self.product_nickname = short_model_to_enum.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS)
994-
robot_new_features = int(self.device.feature_set) if self.device.feature_set else 0
995-
self.device_features = DeviceFeatures.from_feature_flags(
996-
robot_new_features,
997-
self.device.new_feature_set if self.device.new_feature_set is not None else "00000000",
998-
self.product_nickname,
999-
)
762+
self.product_nickname = SHORT_MODEL_TO_ENUM.get(self.model.split(".")[-1], RoborockProductNickname.PEARLPLUS)
1000763

1001764

1002765
@dataclass

0 commit comments

Comments
 (0)