diff --git a/roborock/data/v1/v1_containers.py b/roborock/data/v1/v1_containers.py index 5efc379a..f072d485 100644 --- a/roborock/data/v1/v1_containers.py +++ b/roborock/data/v1/v1_containers.py @@ -224,6 +224,10 @@ def dock_state(self) -> RoborockDockState: newer off-peak charging logic seamlessly while maintaining backwards compatibility with older devices. """ + return self.get_dock_state() + + def get_dock_state(self, *, is_supported_valley_electricity: bool = True) -> RoborockDockState: + """Return the dock state, accounting for off-peak charging support.""" if self.state is None or self.state == RoborockStateCode.unknown: return RoborockDockState.unknown @@ -239,7 +243,7 @@ def dock_state(self) -> RoborockDockState: # 3 & 4. CHARGING and CHARGE_WAITING if self.state == RoborockStateCode.charging: - if self.charge_status == RoborockChargeStatus.charge_waiting: + if is_supported_valley_electricity and self.charge_status == RoborockChargeStatus.charge_waiting: return RoborockDockState.off_peak_waiting return RoborockDockState.charging @@ -250,6 +254,11 @@ def dock_state(self) -> RoborockDockState: # 1. IDLE (Not on dock, or doing something else) return RoborockDockState.idle + @property + def is_battery_charging(self) -> bool: + """Return whether the battery is actively charging.""" + return self.dock_state == RoborockDockState.charging + def __repr__(self) -> str: return _attr_repr(self) diff --git a/roborock/devices/traits/v1/status.py b/roborock/devices/traits/v1/status.py index 9bed4e41..a2e3fa48 100644 --- a/roborock/devices/traits/v1/status.py +++ b/roborock/devices/traits/v1/status.py @@ -5,6 +5,7 @@ from roborock import ( CleaningMode, CleanRoutes, + RoborockDockState, StatusV2, VacuumModes, WaterModes, @@ -65,6 +66,13 @@ def __init__(self, device_feature_trait: DeviceFeaturesTrait, region: str | None self._device_features_trait = device_feature_trait self._region = region + @property + def dock_state(self) -> RoborockDockState: + """Return the feature-aware dock state used by the Roborock app.""" + return self.get_dock_state( + is_supported_valley_electricity=self._device_features_trait.is_supported_valley_electricity + ) + @cached_property def fan_speed_options(self) -> list[VacuumModes]: return get_clean_modes(self._device_features_trait) diff --git a/tests/devices/__snapshots__/test_v1_device.ambr b/tests/devices/__snapshots__/test_v1_device.ambr index 0e5c3f5c..bc0d8fdb 100644 --- a/tests/devices/__snapshots__/test_v1_device.ambr +++ b/tests/devices/__snapshots__/test_v1_device.ambr @@ -876,7 +876,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, cleaning_mode_options=[, , , ], clear_water_box_status=None, collision_avoid_status=None, command=, common_status=None, converter=DefaultConverter, corner_clean_mode=None, current_cleaning_mode=, current_cleaning_mode_name='custom', 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_state=, 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', 108: 'max_plus', 105: 'off', 106: 'custom'}, fan_speed_name='custom', fan_speed_options=[, , , , , , ], has_am=None, 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', 302: 'custom'}, 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: 'mild', 202: 'standard', 203: 'intense', 207: 'custom_water_flow', 204: 'custom'}, water_mode_name='custom', water_mode_options=[, , , , , ], 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, cleaning_mode_options=[, , , ], clear_water_box_status=None, collision_avoid_status=None, command=, common_status=None, converter=DefaultConverter, corner_clean_mode=None, current_cleaning_mode=, current_cleaning_mode_name='custom', 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_state=, 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', 108: 'max_plus', 105: 'off', 106: 'custom'}, fan_speed_name='custom', fan_speed_options=[, , , , , , ], has_am=None, 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_battery_charging=False, 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', 302: 'custom'}, 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: 'mild', 202: 'standard', 203: 'intense', 207: 'custom_water_flow', 204: 'custom'}, water_mode_name='custom', water_mode_options=[, , , , , ], water_shortage_status=None) # --- # name: test_device_trait_command_parsing[status].1 dict({ diff --git a/tests/devices/traits/v1/test_status.py b/tests/devices/traits/v1/test_status.py index 6fb0284c..e96f7701 100644 --- a/tests/devices/traits/v1/test_status.py +++ b/tests/devices/traits/v1/test_status.py @@ -17,6 +17,8 @@ ) from roborock.data import SHORT_MODEL_TO_ENUM, RoborockProductNickname from roborock.data.v1 import ( + RoborockChargeStatus, + RoborockDockState, RoborockStateCode, ) from roborock.device_features import DeviceFeatures @@ -108,6 +110,39 @@ def test_none_values(status_trait: StatusTrait) -> None: assert status_trait.mop_route_name is None +@pytest.mark.parametrize( + ( + "is_supported_valley_electricity", + "charge_status", + "battery", + "expected_dock_state", + "expected_is_battery_charging", + ), + [ + (False, RoborockChargeStatus.charge_waiting, 50, RoborockDockState.charging, True), + (False, None, 50, RoborockDockState.charging, True), + (True, RoborockChargeStatus.charge_waiting, 50, RoborockDockState.off_peak_waiting, False), + (True, RoborockChargeStatus.charging, 50, RoborockDockState.charging, True), + (True, RoborockChargeStatus.charging, 100, RoborockDockState.full, False), + ], +) +def test_feature_aware_dock_and_charging_state( + is_supported_valley_electricity: bool, + charge_status: RoborockChargeStatus | None, + battery: int, + expected_dock_state: RoborockDockState, + expected_is_battery_charging: bool, +) -> None: + """Test dock and charging state against the V1 app's feature-gated logic.""" + status_trait = _create_cleaning_mode_status_trait(is_supported_valley_electricity=is_supported_valley_electricity) + status_trait.state = RoborockStateCode.charging + status_trait.charge_status = charge_status + status_trait.battery = battery + + assert status_trait.dock_state == expected_dock_state + assert status_trait.is_battery_charging is expected_is_battery_charging + + def test_options(status_trait: StatusTrait) -> None: """Test that fan_speed_options returns a list of options.""" status_trait._device_features_trait.is_clean_route_setting_supported = True