Add selective room cleaning to vacuum clean module#1660
Add selective room cleaning to vacuum clean module#1660epg-pers wants to merge 2 commits intopython-kasa:masterfrom
Conversation
Add clean_rooms() to send selective room clean commands using clean_mode 3 with the verified setSwitchClean payload, and get_rooms() to query the room list from the device on demand. Add getMapInfo to the polling query to expose current_map_id. Tested on RV30 Max Plus (EU) firmware 1.3.2.
3a5234c to
cba457a
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1660 +/- ##
==========================================
+ Coverage 93.22% 93.24% +0.01%
==========================================
Files 157 157
Lines 9818 9872 +54
Branches 1003 1010 +7
==========================================
+ Hits 9153 9205 +52
- Misses 472 474 +2
Partials 193 193 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
rytilahti
left a comment
There was a problem hiding this comment.
Thanks for the pull request, @epg-pers! See some initial comments inline.
I am trying to think ahead how to integrate this with downstreams, I guess we would need to add support for parameters for Feature actions to make it easier to integrate, e.g., to home assistant?
| "getBatteryInfo": {}, | ||
| "getCleanStatus": {}, | ||
| "getCleanAttr": {"type": "global"}, | ||
| "getMapInfo": {}, |
There was a problem hiding this comment.
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.
| return await self._change_setting("clean_number", count) | ||
|
|
||
| @property | ||
| def current_map_id(self) -> int: |
There was a problem hiding this comment.
Do we also have the name information? If yes, maybe it makes sense to expose it also?
kasa/smart/modules/clean.py
Outdated
| }, | ||
| ) | ||
|
|
||
| async def get_rooms(self, map_id: int | None = None) -> list[dict]: |
There was a problem hiding this comment.
Instead of returning a list of raw dictionaries, it would be a good idea to parse and store it inside a data class of sorts for nicer downstream use. The code base contains some examples where similar is done, feel free to take a look :-)
kasa/smart/modules/clean.py
Outdated
| return await self.call( | ||
| "setSwitchClean", | ||
| { | ||
| "clean_mode": 3, |
There was a problem hiding this comment.
It would be nice to have the clean mode as an enum for readability, as we know some of the values already.
kasa/smart/modules/clean.py
Outdated
| "force_clean": False, | ||
| "map_id": map_id, | ||
| "room_list": list(room_ids), | ||
| "start_type": 1, |
| """ | ||
| if map_id is None: | ||
| map_id = self.current_map_id | ||
| resp = await self.call("getMapData", {"map_id": map_id, "type": 0}) |
There was a problem hiding this comment.
Any info about the type and its values? Enumization would be nice here, too.
|
Hello! @epg-pers I tested this with my RV30(UN) 1.0, firmware 1.2.2 Build 240627, AES transport, and it worked beautifully! I took a stab at integrating zones too, and it worked, but I would have never managed without your work! In order to progress with different modes, I triggered each cleaning mode from the Tapo app (room, zone, custom, home advanced, home standard) and polled getCleanStatus / getSwitchClean / getVacStatus via the local API to observe which clean_status and clean_mode values the device reported. Then I replayed those values in setSwitchClean payloads locally and verified that the vacuum actually started the expected mode (checked by looking at the vacuum IRL and via the app). One thing I couldn't figure out and trigger properly is the "Home Advanced Cleaning", but the others (spot, rooms, zones) would be covered. In case you're curious, I extended this also trying to factor in @rytilahti's suggestions here master...davidefiocco:python-kasa:feat/room-cleaning-improvements in case it can be useful for inspiration (and zones could be added in a separate PR). It worked for me! |
That's great do hear. It sounds like you followed a similar strategy to me; trigger action from the app > poll the device locally to understand what was set > attempt the same payload locally. Feel free to add your additions to this PR. I haven't had much time this week to revisit @rytilahti suggestions. |
…ding for room cleaning
Sure! @epg-pers, I've prepared a commit addressing @rytilahti's review feedback (enums, dataclasses, base64 name decoding, clean_type property) with rooms only, no zones yet. All 54 tests pass and pre-commit checks are clean. You could either:
And if needed I can try to chime in response to @rytilahti's comments above. |
|
@davidefiocco thanks for doing the legwork on rytilahti's suggestions, ive merged your branch in. 54 tests passing and pre-commit clean. @rytilahti really appreciate the detailed review feedback, the enums, dataclass, and base64 name decoding are all in now. Let me know if there is anything else you would like changed. |
rytilahti
left a comment
There was a problem hiding this comment.
Just a couple of minor nits, nice to see the progress here! Feel free to comment and/or close my previous comments, just to make it visible if they are still pending.
| #: Suction power level (matches :class:`FanSpeed` values). | ||
| suction: int = 0 |
There was a problem hiding this comment.
If the comment is correct, maybe the type of this should be FanSpeed (assuming it's an intenum, currently on mobile)?
| if area.get("type") != AreaType.Room: | ||
| continue |
There was a problem hiding this comment.
Adding debug logging here could be useful when extending this.
| name = raw_name | ||
| rooms.append( |
There was a problem hiding this comment.
Add a newline in-between to create a logical separation.
Summary
Adds support for selective room cleaning to the existing
Cleanmodule:clean_rooms(room_ids: list[int], map_id: int | None = None)- sendssetSwitchCleanwithclean_mode: 3and the verified payload to clean specific roomsget_rooms(map_id: int | None = None)- on-demand call togetMapDatareturning the filtered list of room areascurrent_map_idproperty - exposes the active map ID fromgetMapInfo, which is now included in the polling queryBackground
The correct
setSwitchCleanpayload for selective room cleaning was reverse-engineered by intercepting live device traffic while the official Tapo app performed a room clean (getSwitchCleanreadback). Key findings:clean_mode: 2is spot clean — theroom_listfield is silently ignoredclean_mode: 3is selective room cleanroom_listis a plainint[]of room IDs (the pixel values used in the LZ4 map), not an array of objectsstart_type: 1is requiredThis was verified working on an RV30 Max Plus (EU) running firmware 1.3.2, documented in https://github.com/epg-pers/tapo-rv30-ha and discussed in #1592.
Test plan
test_clean_rooms- verifies correctsetSwitchCleanpayload with default map IDtest_clean_rooms_explicit_map_id- verifies explicitmap_idis used when providedtest_clean_rooms_empty_raises- verifiesValueErroron empty room listtest_get_rooms/test_get_rooms_explicit_map_id- verifiesgetMapDatacall and room filtering