-
-
Notifications
You must be signed in to change notification settings - Fork 263
Add selective room cleaning to vacuum clean module #1660
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: master
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 |
|---|---|---|
|
|
@@ -2,9 +2,11 @@ | |
|
|
||
| from __future__ import annotations | ||
|
|
||
| import base64 | ||
| import logging | ||
| from dataclasses import dataclass | ||
| from datetime import timedelta | ||
| from enum import IntEnum | ||
| from enum import IntEnum, StrEnum | ||
| from typing import Annotated, Literal | ||
|
|
||
| from ...feature import Feature | ||
|
|
@@ -13,6 +15,10 @@ | |
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| # Only known value for start_type in setSwitchClean; required for | ||
| # targeted cleaning modes (Room) but not for StandardHome. | ||
| _START_TYPE_RESUME = 1 | ||
|
|
||
|
|
||
| class Status(IntEnum): | ||
| """Status of vacuum.""" | ||
|
|
@@ -58,6 +64,65 @@ class FanSpeed(IntEnum): | |
| Ultra = 5 | ||
|
|
||
|
|
||
| class CleanMode(IntEnum): | ||
| """Clean mode for ``setSwitchClean`` and ``getCleanStatus``. | ||
|
|
||
| Used as ``clean_mode`` in commands and ``clean_status`` in status responses. | ||
| """ | ||
|
|
||
| #: Clean all rooms with uniform settings. | ||
| StandardHome = 0 | ||
| #: Clean all rooms with per-room settings and custom order. | ||
| AdvancedHome = 1 | ||
| #: Clean a small area around the vacuum's current position. | ||
| Spot = 2 | ||
| #: Clean selected rooms only. | ||
| Room = 3 | ||
| #: Clean user-defined rectangular areas. | ||
| Zone = 4 | ||
| #: Run a saved custom cleaning preset. | ||
| Custom = 5 | ||
|
|
||
|
|
||
| @dataclass | ||
| class CleanAreaSettings: | ||
| """Per-area cleaning settings shared by rooms and zones.""" | ||
|
|
||
| #: Suction power level (matches :class:`FanSpeed` values). | ||
| suction: int = 0 | ||
| #: Water level for mopping. | ||
| cistern: int = 0 | ||
| #: Number of cleaning passes. | ||
| clean_number: int = 0 | ||
|
|
||
|
|
||
| @dataclass | ||
| class RoomInfo(CleanAreaSettings): | ||
| """Information about a room on the vacuum's map.""" | ||
|
|
||
| #: Room ID used in cleaning commands. | ||
| id: int = 0 | ||
| #: Human-readable room name (base64-decoded from the device). | ||
| name: str | None = None | ||
| #: Color index used for map rendering. | ||
| color: int = 0 | ||
|
|
||
|
|
||
| class AreaType(StrEnum): | ||
| """Type of area entry in map data.""" | ||
|
|
||
| #: A named room. | ||
| Room = "room" | ||
| #: A user-defined rectangular cleaning zone. | ||
| Area = "area" | ||
| #: A virtual wall boundary. | ||
| VirtualWall = "virtual_wall" | ||
| #: A no-go zone. | ||
| Forbid = "forbid" | ||
| #: A detected carpet region. | ||
| CarpetRectangle = "carpet_rectangle" | ||
|
|
||
|
|
||
| class AreaUnit(IntEnum): | ||
| """Area unit.""" | ||
|
|
||
|
|
@@ -263,6 +328,7 @@ def query(self) -> dict: | |
| "getBatteryInfo": {}, | ||
| "getCleanStatus": {}, | ||
| "getCleanAttr": {"type": "global"}, | ||
| "getMapInfo": {}, | ||
|
Member
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. Would you mind adding this query also to the dump_devinfos query list, and creating a fixture with the data? That will make it testable and easier to extend in the future. |
||
| } | ||
|
|
||
| async def start(self) -> dict: | ||
|
|
@@ -275,7 +341,7 @@ async def start(self) -> dict: | |
| return await self.call( | ||
| "setSwitchClean", | ||
| { | ||
| "clean_mode": 0, | ||
| "clean_mode": CleanMode.StandardHome, | ||
| "clean_on": True, | ||
| "clean_order": True, | ||
| "force_clean": False, | ||
|
|
@@ -409,3 +475,77 @@ def clean_count(self) -> Annotated[int, FeatureAttribute()]: | |
| async def set_clean_count(self, count: int) -> Annotated[dict, FeatureAttribute()]: | ||
| """Set number of times to clean.""" | ||
| return await self._change_setting("clean_number", count) | ||
|
|
||
| @property | ||
| def current_map_id(self) -> int: | ||
|
Member
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. Do we also have the name information? If yes, maybe it makes sense to expose it also? |
||
| """Return the ID of the currently active map.""" | ||
| return self.data["getMapInfo"]["current_map_id"] | ||
|
|
||
| @property | ||
| def clean_type(self) -> CleanMode | None: | ||
| """Return the active cleaning mode, or ``None`` if unavailable.""" | ||
| cs = self.data.get("getCleanStatus") | ||
| if cs is None or "clean_status" not in cs: | ||
| return None | ||
| return CleanMode(cs["clean_status"]) | ||
|
|
||
| async def clean_rooms( | ||
| self, room_ids: list[int], *, map_id: int | None = None | ||
| ) -> dict: | ||
| """Start cleaning specific rooms. | ||
|
|
||
| Per-room settings are not supported; the device uses the global | ||
| suction / cistern / clean_number values for room cleaning. | ||
|
|
||
| :param room_ids: List of room IDs to clean. | ||
| :param map_id: Map ID to clean on. Defaults to the current active map. | ||
| """ | ||
| if not room_ids: | ||
| raise ValueError("room_ids must not be empty") | ||
| if map_id is None: | ||
| map_id = self.current_map_id | ||
| return await self.call( | ||
| "setSwitchClean", | ||
| { | ||
| "clean_mode": CleanMode.Room, | ||
| "clean_on": True, | ||
| "clean_order": True, | ||
| "force_clean": False, | ||
| "map_id": map_id, | ||
| "room_list": list(room_ids), | ||
| "start_type": _START_TYPE_RESUME, | ||
| }, | ||
| ) | ||
|
|
||
| async def get_rooms(self, map_id: int | None = None) -> list[RoomInfo]: | ||
| """Return the list of rooms for the given map. | ||
|
|
||
| Room names are base64-decoded when present. | ||
|
|
||
| :param map_id: Map ID to query. Defaults to the current active map. | ||
| """ | ||
| if map_id is None: | ||
| map_id = self.current_map_id | ||
| resp = await self.call("getMapData", {"map_id": map_id, "type": 0}) | ||
|
Member
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. Any info about the type and its values? Enumization would be nice here, too. |
||
|
|
||
| rooms: list[RoomInfo] = [] | ||
| for area in resp.get("area_list", []): | ||
| if area.get("type") != AreaType.Room: | ||
| continue | ||
|
Comment on lines
+533
to
+534
Member
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. Adding debug logging here could be useful when extending this. |
||
| name = None | ||
| if raw_name := area.get("name"): | ||
| try: | ||
| name = base64.b64decode(raw_name).decode() | ||
| except Exception: | ||
| name = raw_name | ||
| rooms.append( | ||
|
Comment on lines
+540
to
+541
Member
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. Add a newline in-between to create a logical separation. |
||
| RoomInfo( | ||
| id=area["id"], | ||
| name=name, | ||
| color=area.get("color", 0), | ||
| suction=area.get("suction", 0), | ||
| cistern=area.get("cistern", 0), | ||
| clean_number=area.get("clean_number", 0), | ||
| ) | ||
| ) | ||
| return rooms | ||
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.
If the comment is correct, maybe the type of this should be
FanSpeed(assuming it's an intenum, currently on mobile)?