Repository navigation
feat(v1): expose clean-then-mop status from the device #959
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ | |
| resolve_cleaning_mode, | ||
| ) | ||
| from roborock.devices.traits.common import DpsDataConverter, TraitUpdateListener | ||
| from roborock.exceptions import RoborockException, RoborockUnsupportedFeature | ||
| from roborock.roborock_message import RoborockDataProtocol | ||
| from roborock.roborock_typing import RoborockCommand | ||
|
|
||
|
|
@@ -31,6 +32,8 @@ | |
|
|
||
| _DPS_CONVERTER = DpsDataConverter.from_dataclass(StatusV2) | ||
|
|
||
| _SEQ_TYPE_CLEAN_THEN_MOP = 1 | ||
|
|
||
|
|
||
| class StatusTrait(StatusV2, common.V1TraitMixin, TraitUpdateListener): | ||
| """Trait for managing the status of Roborock devices. | ||
|
|
@@ -49,6 +52,7 @@ class StatusTrait(StatusV2, common.V1TraitMixin, TraitUpdateListener): | |
| - Fan Speed | ||
| - Water Mode | ||
| - Mop Route | ||
| - Clean then mop (``clean_then_mop``, independent of the cleaning mode) | ||
|
|
||
| You should use the _options version of the attribute to know which are | ||
| supported for your device (i.e. fan_speed_options) | ||
|
|
@@ -131,6 +135,32 @@ def mop_route_name(self) -> str | None: | |
| return None | ||
| return self.mop_route_mapping.get(self.mop_mode) | ||
|
|
||
| @property | ||
| def clean_then_mop(self) -> bool | None: | ||
| """Whether each room is vacuumed fully before it is mopped. | ||
|
|
||
| Reported by the device as ``seq_type`` and persisted on the device: it | ||
| tracks the "vacuum then mop" toggle in the vendor app, also while docked. | ||
|
|
||
| This is a separate axis from ``current_cleaning_mode``. The cleaning | ||
| mode is derived from ``fan_power``, ``water_box_mode`` and ``mop_mode`` | ||
| and describes *what* the robot does in a room (vacuum, mop, both). This | ||
| flag describes the *order* when it does both. On the device the two are | ||
| stored independently: writing one leaves the other untouched. Set it | ||
| with ``set_clean_then_mop``. | ||
|
|
||
| The vendor app presents it as a fourth choice next to "vacuum and mop", | ||
| "mop only" and "vacuum only", and clears it when one of those is picked. | ||
| A consumer that wants the same experience should turn this off when it | ||
| selects a cleaning mode other than vacuum and mop; this library does | ||
| not do that implicitly. | ||
|
|
||
| ``None`` means the device does not support the feature. | ||
| """ | ||
| if self.seq_type is None: | ||
| return None | ||
| return self.seq_type == _SEQ_TYPE_CLEAN_THEN_MOP | ||
|
|
||
| @property | ||
| def current_cleaning_mode(self) -> CleaningMode | None: | ||
| return get_current_cleaning_mode( | ||
|
|
@@ -170,6 +200,30 @@ async def resolve_error(self, error_code: int | None = None) -> None: | |
| await self.rpc_channel.send_command(RoborockCommand.RESOLVE_ERROR, params={"error_code": error_code}) | ||
| await self.refresh() | ||
|
|
||
| async def set_clean_then_mop(self, enabled: bool) -> None: | ||
| """Set whether each room is vacuumed fully before it is mopped. | ||
|
|
||
| Independent of ``set_cleaning_mode``. The firmware requires the current | ||
| motor-mode values in this request, so they are echoed back unchanged. | ||
| """ | ||
| if not self._device_features_trait.is_clean_then_mop_mode_supported: | ||
| raise RoborockUnsupportedFeature("Clean then mop is not supported") | ||
| if self.fan_power is None or self.water_box_mode is None: | ||
| raise RoborockException("Status is not loaded, call refresh() first") | ||
| seq_type = _SEQ_TYPE_CLEAN_THEN_MOP if enabled else 0 | ||
| params: dict[str, int] = { | ||
| "type": seq_type, | ||
| "fan_power": self.fan_power, | ||
| "water_box_mode": self.water_box_mode, | ||
| } | ||
| if self.mop_mode is not None: | ||
| params["mop_mode"] = self.mop_mode | ||
| if enabled and self._device_features_trait.is_ctm_with_repeat_supported: | ||
| params["repeat"] = 1 | ||
| await self.rpc_channel.send_command(RoborockCommand.APP_SET_CLEAN_SEQUENCE_TYPE, params=params) | ||
| self.seq_type = seq_type | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you see if we need self._notify_update() here?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. Home Assistant's coordinator subscribes to the status trait's update listener, so without it the optimistic value only shows up after the next poll. Added in 15c0db8, with the test asserting a listener fires. |
||
| self._notify_update() | ||
|
|
||
| def update_from_dps(self, decoded_dps: dict[RoborockDataProtocol, Any]) -> None: | ||
| """Update the trait from data protocol push message data. | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might need to guard against fan power or water box mode being none here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added in 15c0db8: raises
RoborockException("Status is not loaded, call refresh() first")when either isNone, before anything is sent.paramsis nowdict[str, int]. Test added.