Skip to content
Open
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_containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is it ok to have two versions, one with an extra input and one not? I think it implies that this method be determined by the container, since it needs device features as input.

I would say: (1) the current dock_state property needs to be deprecated and (2) the method below should be a function that takes as input state, battery, and is_supported_valley_electricity and is called from the trait.

(What is in this PR works in practice because the trait overrides the dock_state method, so I can approve, but i think this should be cleaned up)


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

Expand All @@ -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

Expand All @@ -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)

Expand Down
8 changes: 8 additions & 0 deletions roborock/devices/traits/v1/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from roborock import (
CleaningMode,
CleanRoutes,
RoborockDockState,
StatusV2,
VacuumModes,
WaterModes,
Expand Down Expand Up @@ -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)
Expand Down
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 @@ -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=[<CleaningMode.VACUUM: 'vacuum'>, <CleaningMode.VAC_AND_MOP: 'vac_and_mop'>, <CleaningMode.MOP: 'mop'>, <CleaningMode.CUSTOM: 'custom'>], clear_water_box_status=None, collision_avoid_status=None, command=<RoborockCommand.GET_STATUS: 'get_status'>, common_status=None, converter=DefaultConverter, corner_clean_mode=None, current_cleaning_mode=<CleaningMode.CUSTOM: 'custom'>, 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=<RoborockDockState.full: 'full'>, 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', 108: 'max_plus', 105: 'off', 106: 'custom'}, fan_speed_name='custom', fan_speed_options=[<VacuumModes.QUIET: 'quiet'>, <VacuumModes.BALANCED: 'balanced'>, <VacuumModes.TURBO: 'turbo'>, <VacuumModes.MAX: 'max'>, <VacuumModes.MAX_PLUS: 'max_plus'>, <VacuumModes.OFF: 'off'>, <VacuumModes.CUSTOMIZED: 'custom'>], has_am=None, 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', 302: 'custom'}, mop_route_name=None, mop_route_options=[<CleanRoutes.STANDARD: 'standard'>, <CleanRoutes.DEEP: 'deep'>, <CleanRoutes.CUSTOMIZED: 'custom'>], 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: 'mild', 202: 'standard', 203: 'intense', 207: 'custom_water_flow', 204: 'custom'}, water_mode_name='custom', water_mode_options=[<WaterModes.OFF: 'off'>, <WaterModes.MILD: 'mild'>, <WaterModes.STANDARD: 'standard'>, <WaterModes.INTENSE: 'intense'>, <WaterModes.CUSTOM: 'custom_water_flow'>, <WaterModes.CUSTOMIZED: 'custom'>], 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=[<CleaningMode.VACUUM: 'vacuum'>, <CleaningMode.VAC_AND_MOP: 'vac_and_mop'>, <CleaningMode.MOP: 'mop'>, <CleaningMode.CUSTOM: 'custom'>], clear_water_box_status=None, collision_avoid_status=None, command=<RoborockCommand.GET_STATUS: 'get_status'>, common_status=None, converter=DefaultConverter, corner_clean_mode=None, current_cleaning_mode=<CleaningMode.CUSTOM: 'custom'>, 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=<RoborockDockState.full: 'full'>, 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', 108: 'max_plus', 105: 'off', 106: 'custom'}, fan_speed_name='custom', fan_speed_options=[<VacuumModes.QUIET: 'quiet'>, <VacuumModes.BALANCED: 'balanced'>, <VacuumModes.TURBO: 'turbo'>, <VacuumModes.MAX: 'max'>, <VacuumModes.MAX_PLUS: 'max_plus'>, <VacuumModes.OFF: 'off'>, <VacuumModes.CUSTOMIZED: 'custom'>], has_am=None, 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_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=[<CleanRoutes.STANDARD: 'standard'>, <CleanRoutes.DEEP: 'deep'>, <CleanRoutes.CUSTOMIZED: 'custom'>], 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: 'mild', 202: 'standard', 203: 'intense', 207: 'custom_water_flow', 204: 'custom'}, water_mode_name='custom', water_mode_options=[<WaterModes.OFF: 'off'>, <WaterModes.MILD: 'mild'>, <WaterModes.STANDARD: 'standard'>, <WaterModes.INTENSE: 'intense'>, <WaterModes.CUSTOM: 'custom_water_flow'>, <WaterModes.CUSTOMIZED: 'custom'>], water_shortage_status=None)
# ---
# name: test_device_trait_command_parsing[status].1
dict({
Expand Down
35 changes: 35 additions & 0 deletions tests/devices/traits/v1/test_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading