Skip to content

dumping HTTP POST Body for Tapo Vacuum (RV30 Plus) #937

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

Closed
steveredden opened this issue May 25, 2024 · 13 comments · Fixed by #944
Closed

dumping HTTP POST Body for Tapo Vacuum (RV30 Plus) #937

steveredden opened this issue May 25, 2024 · 13 comments · Fixed by #944
Labels
enhancement New feature or request no-stale

Comments

@steveredden
Copy link
Contributor

MITM captured these from the tapo app to a RV30 Plus. just wanted to dump them for the public.

{
    "method": "multipleRequest",
    "params": {
        "requests": [
            {
                "method": "getVacStatus"
            },
            {
                "method": "getBatteryInfo"
            },
            {
                "method": "getComponentList"
            },
            {
                "method": "getDeviceInfo"
            }
        ]
    }
}

START CLEANING A SPECIFIC ROOM:

{
    "method": "multipleRequest",
    "params": {
        "requests": [
            {
                "method": "setSwitchClean",
                "params": {
                    "clean_mode": 3,
                    "clean_on": true,
                    "clean_order": true,
                    "force_clean": false,
                    "map_id": 1687964172,
                    "room_list": [
                        1
                    ]
                }
            }
        ]
    }
}
{
    "method": "multipleRequest",
    "params": {
        "requests": [
            {
                "method": "getAutoChangeMap"
            }
        ]
    }
}
{
    "method": "multipleRequest",
    "params": {
        "requests": [
            {
                "method": "getMapInfo"
            },
            {
                "method": "startUpdateMapData",
                "params": {
                    "map_upload_start": true
                }
            },
            {
                "method": "getAreaUnit"
            }
        ]
    }
}
{
    "method": "multipleRequest",
    "params": {
        "requests": [
            {
                "method": "getMapData",
                "params": {
                    "map_id": -1
                }
            },
            {
                "method": "getPathData",
                "params": {
                    "start_pos": 0
                }
            }
        ]
    }
}

GET REPORTS

{
    "method": "multipleRequest",
    "params": {
        "requests": [
            {
                "method": "getCleanRecords"
            }
        ]
    }
}

HEAD TO DOCK

{
    "method": "multipleRequest",
    "params": {
        "requests": [
            {
                "method": "setSwitchCharge",
                "params": {
                    "switch_charge": true
                }
            }
        ]
    }
}

STOP HEADING TO DOCK

{
    "method": "multipleRequest",
    "params": {
        "requests": [
            {
                "method": "setSwitchCharge",
                "params": {
                    "switch_charge": false
                }
            }
        ]
    }
}

CLEAN WHOLE MAP

{
    "method": "multipleRequest",
    "params": {
        "requests": [
            {
                "method": "setSwitchClean",
                "params": {
                    "clean_mode": 0,
                    "clean_on": true,
                    "clean_order": true,
                    "force_clean": false
                }
            }
        ]
    }
}

PAUSE

{
    "method": "multipleRequest",
    "params": {
        "requests": [
            {
                "method": "setRobotPause",
                "params": {
                    "pause": true
                }
            }
        ]
    }
}

RESUME

{
    "method": "multipleRequest",
    "params": {
        "requests": [
            {
                "method": "setRobotPause",
                "params": {
                    "pause": false
                }
            }
        ]
    }
}

CHANGE THE NUMBER OF PASSES

{
    "method": "multipleRequest",
    "params": {
        "requests": [
            {
                "method": "setCleanAttr",
                "params": {
                    "cistern": 2,
                    "clean_number": 2,
                    "suction": 2,
                    "type": "global"
                }
            }
        ]
    }
}

START COLLECTING DUST

{
    "method": "multipleRequest",
    "params": {
        "requests": [
            {
                "method": "setSwitchDustCollection",
                "params": {
                    "switch_dust_collection": true
                }
            }
        ]
    }
}

START ZONE CLEANING

{
    "method": "multipleRequest",
    "params": {
        "requests": [
            {
                "method": "setSwitchClean",
                "params": {
                    "area_list": [
                        {
                            "cistern": 2,
                            "clean_number": 1,
                            "id": 0,
                            "name": "",
                            "suction": 2,
                            "tag": "",
                            "type": "area",
                            "vertexs": [
                                [
                                    -1861,
                                    2262
                                ],
                                [
                                    653,
                                    2262
                                ],
                                [
                                    653,
                                    -1701
                                ],
                                [
                                    -1861,
                                    -1701
                                ]
                            ]
                        }
                    ],
                    "clean_mode": 4,
                    "clean_on": true,
                    "clean_order": true,
                    "force_clean": false,
                    "map_id": 1687964172
                }
            }
        ]
    }
}
@rytilahti
Copy link
Member

rytilahti commented May 25, 2024

Nice! Would you mind trying kasa --debug discover and post the discovery payload, or even trying to obtain a fixture dump (https://python-kasa.readthedocs.io/en/latest/contribute.html#contributing-fixture-files)?

@steveredden
Copy link
Contributor Author

== Unsupported device ==
        == Discovery Result ==
        Device Type:        SMART.TAPOROBOVAC
        Device Model:       RV30 Plus(US)
        IP:                 192.168.1.202
        MAC:                AC-15-A2-72-B9-84
        Device Id (hash):   a1e6009e771ecc9876a7b7ad7b282d39
        Owner (hash):       48D105C5593D5366DFCE115DCE5C3A06
        HW Ver:             None
        Supports IOT Cloud: True
        OBD Src:            tplink
        Factory Default:    False
        Encrypt Type:       AES
        Supports HTTPS:     True
        HTTP Port:          4433
        LV (Login Level):   None

dump bombs out pretty early.

Host given, performing discovery on 192.168.1.202.
Traceback (most recent call last):
  File "C:\Users\Steve\Documents\Development\python\python-kasa\kasa\deviceconfig.py", line 99, in from_values
    DeviceFamilyType(device_family),
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Python311\Lib\enum.py", line 711, in __call__
    return cls.__new__(cls, value)
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Python311\Lib\enum.py", line 1128, in __new__
    raise ve_exc
ValueError: 'SMART.TAPOROBOVAC' is not a valid DeviceFamilyType

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "C:\Users\Steve\Documents\Development\python\python-kasa\kasa\discover.py", line 466, in _get_device_instance
    config.connection_type = ConnectionType.from_values(
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Steve\Documents\Development\python\python-kasa\kasa\deviceconfig.py", line 104, in from_values
    raise KasaException(
kasa.exceptions.KasaException: Invalid connection parameters for SMART.TAPOROBOVAC.AES.None

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "C:\Users\Steve\Documents\Development\python\python-kasa\devtools\dump_devinfo.py", line 696, in <module>
    cli()
  File "C:\Users\Steve\Documents\Development\python\kasaenv\Lib\site-packages\asyncclick\core.py", line 1205, in __call__
    return anyio.run(self._main, main, args, kwargs, **opts)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Steve\Documents\Development\python\kasaenv\Lib\site-packages\anyio\_core\_eventloop.py", line 73, in run
    return async_backend.run(func, args, {}, backend_options)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Steve\Documents\Development\python\kasaenv\Lib\site-packages\anyio\_backends\_asyncio.py", line 2001, in run
    return runner.run(wrapper())
           ^^^^^^^^^^^^^^^^^^^^^
  File "C:\Python311\Lib\asyncio\runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Python311\Lib\asyncio\base_events.py", line 653, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "C:\Users\Steve\Documents\Development\python\kasaenv\Lib\site-packages\anyio\_backends\_asyncio.py", line 1989, in wrapper
    return await func(*args)
           ^^^^^^^^^^^^^^^^^
  File "C:\Users\Steve\Documents\Development\python\kasaenv\Lib\site-packages\asyncclick\core.py", line 1208, in _main
    return await main(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Steve\Documents\Development\python\kasaenv\Lib\site-packages\asyncclick\core.py", line 1120, in main
    rv = await self.invoke(ctx)
         ^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Steve\Documents\Development\python\kasaenv\Lib\site-packages\asyncclick\core.py", line 1485, in invoke
    return await ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Steve\Documents\Development\python\kasaenv\Lib\site-packages\asyncclick\core.py", line 824, in invoke
    rv = await rv
         ^^^^^^^^
  File "C:\Users\Steve\Documents\Development\python\python-kasa\devtools\dump_devinfo.py", line 253, in cli
    device = await Discover.discover_single(
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Steve\Documents\Development\python\python-kasa\kasa\discover.py", line 396, in discover_single
    raise protocol.unsupported_device_exceptions[ip]
  File "C:\Users\Steve\Documents\Development\python\python-kasa\kasa\discover.py", line 160, in datagram_received
    device = Discover._get_device_instance(data, config)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Steve\Documents\Development\python\python-kasa\kasa\discover.py", line 472, in _get_device_instance
    raise UnsupportedDeviceError(
kasa.exceptions.UnsupportedDeviceError: Unsupported device 192.168.1.202 of type SMART.TAPOROBOVAC with encrypt_type AES

Are you looking to support vacuums in this project? How about Tapo Doorbells?

@steveredden
Copy link
Contributor Author

vacuum starts with a login POST with MD5 of password and username

image

response is a token:

{
    "error_code": 0,
    "result": {
        "token": "9A07686B66FB6A4E325DE4250D509B51"
    }
}

which is then used in all calls:

image

@rytilahti
Copy link
Member

Thanks for the discover output and clarifications!

Looks like the transport protocol is already implemented in this library (aes transport), but this is the first device known to use HTTPS, so some changes might be necessary to support it properly.

Are you looking to support vacuums in this project?

For sure! As adding (at least basic) support should be rather straightforward, I don't personally see any reason not to. If you modify

class DeviceFamilyType(Enum):
to include SMART.TAPOROBOVAC, it shouldn't crash anymore and you could execute commands like kasa command getVacStatus to fetch the state.

If that works, it would be helpful if you could you try kasa command component_nego and kasa command get_device_info commands (or just re-try again with the dump_devinfo, which executes these, too).

How about Tapo Doorbells?

The current master branch has some preliminary support for hubs and some "passive" devices (like temp, door & contact sensors), but there is no support to control device-to-device triggers which is probably required to add proper support for doorbells.

What is already possible with #900 is to poll the event logs, but it'd be great if you could dump traffic what happens when you set up a "doorbell -> do some action".

@steveredden
Copy link
Contributor Author

your initial hesitations seem to be warranted.

without --port

Host given, performing discovery on 192.168.1.202.
Unknown SMART device with SMART.TAPOROBOVAC, using SmartDevice
Testing component_nego call ..Unable to query component_nego call at once: ('Device connection error: 192.168.1.202: Cannot connect to host 192.168.1.202:80 ssl:default [The remote computer refused the network connection]', ClientConnectorError(ConnectionKey(host='192.168.1.202', port=80, is_ssl=False, ssl=True, proxy=None, proxy_auth=None, proxy_headers_hash=None), ConnectionRefusedError(22, 'The remote computer refused the network connection', None, 1225, None)))

with --port 4433

Host given, performing discovery on 192.168.1.202.
Fatal write error on datagram transport
protocol: <kasa.discover._DiscoverProtocol object at 0x0000021A0F6526D0>
transport: <_ProactorDatagramTransport fd=744 read=<_OverlappedFuture pending cb=[_ProactorDatagramTransport._loop_reading()]>>
Traceback (most recent call last):
  File "C:\Python311\Lib\asyncio\proactor_events.py", line 536, in _loop_writing
    self._write_fut = self._loop._proactor.sendto(self._sock,
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Python311\Lib\asyncio\windows_events.py", line 548, in sendto
    ov.WSASendTo(conn.fileno(), buf, flags, addr)
TypeError: 'str' object cannot be interpreted as an integer
.
.
.

@steveredden
Copy link
Contributor Author

And pardon my misspeaking - I actually meant Tapo Door Locks - they're beta testing them now.

@rytilahti
Copy link
Member

Oops, sorry about that. The linked PR fixes the incorrect type for port number, so the script might work if you apply that patch in your local copy.

However, looking at the first requests in the description, it might be that none of the commands used by the script will yield any results (other devices uses snake_case and the vacuum camelCase for the names like get_device_info and get_components, two commands whose output would interesting), but it's worth trying.

Did you try using kasa command <one of those commands> already or did it have?

On those locks, assuming that they use the same protocol which they likely do, I see why we couldn't add support for those in the future, too. The upcoming 0.7 release makes the library more modularized which open an easy way to support new devices.

@steveredden
Copy link
Contributor Author

Sent you a discord message: any way to not verify ssl certs? Do we need a new parameter to expose that?

@steveredden
Copy link
Contributor Author

attaching common request + responses

responses.zip

@rytilahti
Copy link
Member

rytilahti commented May 26, 2024

We discussed about this on discord, but here's basically what we know:

  • Supporting this device requires implementing a new transport class. username + hashed password are send inside https tunnel to the device, which responds with a token that needs to be used for future requests.
  • The discovery response says AES even when the payloads themselves are not encrypted, furthermore, there is no clean indicator for this different transport protocol (besides port number & that it uses https, maybe?).
  • The method names are camelCased like getDeviceInfo instead of get_device_info. the snake_cased names work also just as well.
  • The getComponentList responds with a similar payload as component_nego does. component_nego works also, so we just need to implement the new transport protocol & add new modules to support the vacuuming features.

@rytilahti rytilahti added the enhancement New feature or request label May 26, 2024
Copy link

There hasn't been any activity on this issue recently. This issue has been automatically marked as stale because of that. It will be closed if no further activity occurs.
Please make sure to update to the latest python-kasa version and check if that solves the issue.
Thank you for your contributions.

@github-actions github-actions bot added the stale label Sep 28, 2024
@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Oct 6, 2024
@rytilahti rytilahti reopened this Oct 6, 2024
@github-actions github-actions bot removed the stale label Oct 7, 2024
rytilahti added a commit that referenced this issue Dec 1, 2024
This PR implements a clear-text, token-based transport protocol seen on
RV30 Plus (#937).

- Client sends `{"username": "email@example.com", "password":
md5(password)}` and gets back a token in the response
- Rest of the communications are done with POST at `/app?token=<token>`

---------

Co-authored-by: Steven B. <51370195+sdb9696@users.noreply.github.com>
@steveredden
Copy link
Contributor Author

some more reverse engineered methods
more methods.txt

@rytilahti
Copy link
Member

Thanks a lot for your input, testing & example commands, @steveredden! I think we now have some sort of implementation for most of the known, interesting commands (be it already merged in, or in open PRs) to fulfill the basic, initial implementation :-)

Notably missing are:

  • Child lock
  • Spot/zone cleaning
  • Do not disturb mode

sdb9696 added a commit that referenced this issue Jan 26, 2025
## [0.10.0](https://github.com/python-kasa/python-kasa/tree/0.10.0) (2025-01-26)

[Full Changelog](0.9.1...0.10.0)

**Release summary:**

This release brings support for many new devices, including completely new device types:

- Support for Tapo robot vacuums. Special thanks to @steveredden, @MAXIGAMESSUPPER, and veep60 for helping to get this implemented!
- Support for hub attached cameras and doorbells (H200)
- Improved support for hubs (including pairing & better chime controls)
- Support for many new camera and doorbell device models, including C220, C720, D100C, D130, and D230

Many thanks to testers and new contributors - @steveredden, @DawidPietrykowski, @Obbay2, @andrewome, @ryenitcher and @etmmvdp!

**Breaking changes:**

- `uses_http` is now a readonly property of device config. Consumers that relied on `uses_http` to be persisted with `DeviceConfig.to_dict()` will need to store the value separately.
- `is_color`, `is_dimmable`, `is_variable_color_temp`, `valid_temperate_range`, and `has_effects` attributes from the `Light` module are deprecated, consumers should use `has_feature("hsv")`, `has_feature("brightness")`, `has_feature("color_temp")`, `get_feature("color_temp").range`, and `Module.LightEffect in dev.modules` respectively. Calling the deprecated attributes will emit a `DeprecationWarning` and type checkers will fail them.
-  `alarm_volume` on the `smart.Alarm` module is changed from `str` to `int`

**Breaking changes:**

- Make uses\_http a readonly property of device config [\#1449](#1449) (@sdb9696)
- Allow passing alarm parameter overrides [\#1340](#1340) (@rytilahti)
- Deprecate legacy light module is\_capability checks [\#1297](#1297) (@sdb9696)

**Implemented enhancements:**

- Expose more battery sensors for D230 [\#1451](#1451)
- dumping HTTP POST Body for Tapo Vacuum \(RV30 Plus\) [\#937](#937)
- Add common alarm interface [\#1479](#1479) (@sdb9696)
- Add common childsetup interface [\#1470](#1470) (@sdb9696)
- Add childsetup module to smartcam hubs [\#1469](#1469) (@sdb9696)
- Add smartcam pet detection toggle module [\#1465](#1465) (@DawidPietrykowski)
- Only log one warning per unknown clean error code and status [\#1462](#1462) (@rytilahti)
- Add childlock module for vacuums [\#1461](#1461) (@rytilahti)
- Add ultra mode \(fanspeed = 5\) for vacuums [\#1459](#1459) (@rytilahti)
- Add setting to change carpet clean mode [\#1458](#1458) (@rytilahti)
- Add setting to change clean count [\#1457](#1457) (@rytilahti)
- Add mop module [\#1456](#1456) (@rytilahti)
- Enable dynamic hub child creation and deletion on update [\#1454](#1454) (@sdb9696)
- Expose current cleaning information [\#1453](#1453) (@rytilahti)
- Add battery module to smartcam devices [\#1452](#1452) (@sdb9696)
- Allow update of camera modules after setting values [\#1450](#1450) (@sdb9696)
- Update hub children on first update and delay subsequent updates [\#1438](#1438) (@sdb9696)
- Add support for doorbells and chimes [\#1435](#1435) (@steveredden)
- Implement vacuum dustbin module \(dust\_bucket\) [\#1423](#1423) (@rytilahti)
- Allow https for klaptransport [\#1415](#1415) (@rytilahti)
- Add smartcam child device support for smartcam hubs [\#1413](#1413) (@sdb9696)
- Add powerprotection module [\#1337](#1337) (@rytilahti)
- Add vacuum speaker controls [\#1332](#1332) (@rytilahti)
- Add consumables module for vacuums [\#1327](#1327) (@rytilahti)
- Add ADC Value to PIR Enabled Switches [\#1263](#1263) (@ryenitcher)
- Add support for cleaning records [\#945](#945) (@rytilahti)
- Initial support for vacuums \(clean module\) [\#944](#944) (@rytilahti)
- Add support for pairing devices with hubs [\#859](#859) (@rytilahti)

**Fixed bugs:**

- TP-Link HS300 Wi-Fi Power-Strip - "Parent On/Off" not functioning. [\#637](#637)
- Convert carpet\_clean\_mode to carpet\_boost switch [\#1486](#1486) (@rytilahti)
- Change category for empty dustbin feature from Primary to Config [\#1485](#1485) (@rytilahti)
- Report 0 for instead of None for zero current and voltage [\#1483](#1483) (@ryenitcher)
- Disable iot camera creation until more complete [\#1480](#1480) (@sdb9696)
- ssltransport: use debug logger for sending requests [\#1443](#1443) (@rytilahti)
- Fix discover cli command with host [\#1437](#1437) (@sdb9696)
- Fallback to is\_low for batterysensor's battery\_low [\#1420](#1420) (@rytilahti)
- Fix iot strip turn on and off from parent [\#639](#639) (@Obbay2)

**Added support for devices:**

- Add D130\(US\) 1.0 1.1.9 fixture [\#1476](#1476) (@sdb9696)
- Add D100C\(US\) 1.0 1.1.3 fixture [\#1475](#1475) (@sdb9696)
- Add C220\(EU\) 1.0 1.2.2 camera fixture [\#1466](#1466) (@DawidPietrykowski)
- Add D230\(EU\) 1.20 1.1.19 fixture [\#1448](#1448) (@sdb9696)
- Add fixture for C720 camera [\#1433](#1433) (@steveredden)

**Project maintenance:**

- Update ruff to 0.9 [\#1482](#1482) (@sdb9696)
- Cancel in progress CI workflows after new pushes [\#1481](#1481) (@sdb9696)
- Update test framework to support smartcam device discovery. [\#1477](#1477) (@sdb9696)
- Add error code 7 for clean module [\#1474](#1474) (@rytilahti)
- Enable CI workflow on PRs to feat/ fix/ and janitor/ [\#1471](#1471) (@sdb9696)
- Add commit-hook to prettify JSON files [\#1455](#1455) (@rytilahti)
- Add required sphinx.configuration [\#1446](#1446) (@rytilahti)
- Add more redactors for smartcams [\#1439](#1439) (@sdb9696)
- Add KS230\(US\) 2.0 1.0.11 IOT Fixture [\#1430](#1430) (@ZeliardM)
- Add tests for dump\_devinfo parent/child smartcam fixture generation [\#1428](#1428) (@sdb9696)
- Raise errors on single smartcam child requests [\#1427](#1427) (@sdb9696)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request no-stale
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants