Skip to content

Add selective room cleaning to vacuum clean module#1660

Open
epg-pers wants to merge 2 commits intopython-kasa:masterfrom
epg-pers:feature/vacuum-room-cleaning
Open

Add selective room cleaning to vacuum clean module#1660
epg-pers wants to merge 2 commits intopython-kasa:masterfrom
epg-pers:feature/vacuum-room-cleaning

Conversation

@epg-pers
Copy link

Summary

Adds support for selective room cleaning to the existing Clean module:

  • clean_rooms(room_ids: list[int], map_id: int | None = None) - sends setSwitchClean with clean_mode: 3 and the verified payload to clean specific rooms
  • get_rooms(map_id: int | None = None) - on-demand call to getMapData returning the filtered list of room areas
  • current_map_id property - exposes the active map ID from getMapInfo, which is now included in the polling query

Background

The correct setSwitchClean payload for selective room cleaning was reverse-engineered by intercepting live device traffic while the official Tapo app performed a room clean (getSwitchClean readback). Key findings:

  • clean_mode: 2 is spot clean — the room_list field is silently ignored
  • clean_mode: 3 is selective room clean
  • room_list is a plain int[] of room IDs (the pixel values used in the LZ4 map), not an array of objects
  • start_type: 1 is required

This 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

  • All 48 existing + new tests pass against both RV20 and RV30 fixtures
  • test_clean_rooms - verifies correct setSwitchClean payload with default map ID
  • test_clean_rooms_explicit_map_id - verifies explicit map_id is used when provided
  • test_clean_rooms_empty_raises - verifies ValueError on empty room list
  • test_get_rooms / test_get_rooms_explicit_map_id - verifies getMapData call and room filtering

@epg-pers epg-pers mentioned this pull request Feb 23, 2026
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.
@epg-pers epg-pers force-pushed the feature/vacuum-room-cleaning branch from 3a5234c to cba457a Compare February 23, 2026 22:00
@codecov
Copy link

codecov bot commented Feb 23, 2026

Codecov Report

❌ Patch coverage is 96.55172% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.24%. Comparing base (932f3e2) to head (a8819f3).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
kasa/smart/modules/clean.py 96.55% 2 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Member

@rytilahti rytilahti left a comment

Choose a reason for hiding this comment

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

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": {},
Copy link
Member

Choose a reason for hiding this comment

The 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.

return await self._change_setting("clean_number", count)

@property
def current_map_id(self) -> int:
Copy link
Member

Choose a reason for hiding this comment

The 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?

},
)

async def get_rooms(self, map_id: int | None = None) -> list[dict]:
Copy link
Member

Choose a reason for hiding this comment

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

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 :-)

return await self.call(
"setSwitchClean",
{
"clean_mode": 3,
Copy link
Member

Choose a reason for hiding this comment

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

It would be nice to have the clean mode as an enum for readability, as we know some of the values already.

"force_clean": False,
"map_id": map_id,
"room_list": list(room_ids),
"start_type": 1,
Copy link
Member

Choose a reason for hiding this comment

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

Should this also be an enum?

"""
if map_id is None:
map_id = self.current_map_id
resp = await self.call("getMapData", {"map_id": map_id, "type": 0})
Copy link
Member

Choose a reason for hiding this comment

The 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.

@davidefiocco
Copy link
Contributor

davidefiocco commented Feb 28, 2026

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!

@epg-pers
Copy link
Author

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.

@davidefiocco
Copy link
Contributor

davidefiocco commented Mar 1, 2026

Feel free to add your additions to this PR.

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 can see the diff here: epg-pers/python-kasa@feature/vacuum-room-cleaning...davidefiocco:python-kasa:feature/vacuum-room-cleaning-review-fixes

You could either:

  • Add me as a collaborator on your fork so I can push directly to your branch, or
  • Pull my changes into your branch:
git remote add davidefiocco https://github.com/davidefiocco/python-kasa.git
git fetch davidefiocco feature/vacuum-room-cleaning-review-fixes
git merge davidefiocco/feature/vacuum-room-cleaning-review-fixes

And if needed I can try to chime in response to @rytilahti's comments above.

@epg-pers
Copy link
Author

epg-pers commented Mar 2, 2026

@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.

Copy link
Member

@rytilahti rytilahti left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines +91 to +92
#: Suction power level (matches :class:`FanSpeed` values).
suction: int = 0
Copy link
Member

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)?

Comment on lines +533 to +534
if area.get("type") != AreaType.Room:
continue
Copy link
Member

Choose a reason for hiding this comment

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

Adding debug logging here could be useful when extending this.

Comment on lines +540 to +541
name = raw_name
rooms.append(
Copy link
Member

Choose a reason for hiding this comment

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

Add a newline in-between to create a logical separation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants