360 Smart Vacuum
I recently purchased a used broken 360 Smart Life S7 vacuum cleaner. It’s an older model, and doesn’t have any exposed API for control. All commands are sent via their propriatary app through the cloud.
Just want to quickly connect to your home automation system? Premade configurations are at the end of the article or you can click to jump straight to Home Assistant or Homebridge.
My initial plan was just to keep it offline entirely and stuff an ESP32 in it’s head to remotely press the buttons and detect it’s state, but I thought it would be a great opportunity to practice reversing Android applications, which I have little experience doing. This project had lots of new hurdles to jump, like patching out SSL pinning, hardcoded IP addresses, obfuscators/packers, broken crypto, and general weirdness.
This documentation is not complete and will likely remain so; this vaccum is now named Dusty and lives at my mom’s place, happly working with her Homebridge setup. Many of the APIs like retrieving mapping data are not documented, but core functionality (eg. triggering a cleaning) works great.
Table of Contents
Obtaining Keys
A QID (Qihoo ID?) and SID (Session ID?), and vacuum SN are required to send commands to your vacuum.
QID:
This is your user account number:
- Sign into the 360 website using this link: https://i.360.cn/reg/?src=pcw_open_app&destUrl=http%3A%2F%2Fdev.app.360.cn%2Fapp%2Flist
- Visit http://open.app.360.cn/developer/ and copy the ten digit number beginning in
360Uxxxxxxxx
at the top right. This is your QID. It should look like this:3003705430
Obtaining the SID:
The SID is a 32-character long session ID used to authenticate HTTP requests to the API. Obtaining this key is currently non-trivial and will be documented after a better method is found for extracting it.
Previously the only way to obtain the SID was to patch out the SSL pinning and intercept the HTTPS connections to the server. Convieniently, these keys are dumped right into the app’s logs and can be accessed using ADB.
If you have an Android phone or emulator with ADB setup, you can run the following command on your PC to dump the keys from the app’s logs. (facepalm!) It may take a moment, to speed it along you can interact with the 360 app.
1adb logcat | grep -m 1 MyPushMessageListener.java | tail -c 93
It will output the QID, SID, and the Push Key. Save the push key, as this may be useful in the future. It is required for some data like battery status that uses the push server.
Generating a cookie
Once you have your SID and UID, you can use the following cookie to authenticate:
1q=u=&t=1;t=&v=2.0&a=1; qid=1234123412; sid=4a20145c62428d31b52b53c9ccbfcee4
The q=u=...
part is required, don’t change it.
Obtaining the SN:
The SN can be viewed in Toolbox->Settings->Device Info on the app. You can also use the API to get a list of vaccums assosciated with your account using the command below.
1curl -X POST \
2 https://q.smart.360.cn/common/dev/GetList \
3 -H 'Cookie: q=u=&t=1;t=&v=2.0&a=1; qid=1234123412; sid=4a20145c62428d31b52b53c9ccbfcee4' \
If you have multiple vacuumss, this is how you distinguish between them when sending API commands. It is required even if you only have one.
Sending commands
With the above info you can now send arbitrary commands to the vacuum. Most commands are send to the clean/cmd/send
endpoint. Here is an example of the “Find Robot” command, which makes it speak.
1curl -X POST \
2 https://q.smart.360.cn/clean/cmd/send \
3 -H 'Content-Type: application/x-www-form-urlencoded' \
4 -H 'Host: q.smart.360.cn' \
5 -H 'Connection: Keep-Alive' \
6 -H 'Accept-Encoding: gzip' \
7 -H 'Cookie: q=u=&t=1;t=&v=2.0&a=1; qid=1234123412; sid=4a20145c62428d31b52b53c9ccbfcee4' \
8 -d 'sn=361TY*******542&infoType=21020&data=%7B%22ctrlCode%22%3A3010%7D&devType=3'
1POST /clean/cmd/send HTTP/1.1
2Content-Type: application/x-www-form-urlencoded
3Content-Length: 234
4Host: q.smart.360.cn
5Connection: Keep-Alive
6Accept-Encoding: gzip
7Cookie: q=u=&t=1;t=&v=2.0&a=1; qid=1234123412; sid=4a20145c62428d31b52b53c9ccbfcee4
8
9sn=361TY*******542&infoType=21020&data=%7B%22ctrlCode%22%3A3010%7D&devType=3
Command List (Main API)
A full list of known commands is here:
https://github.com/stonegray/360smartai/blob/master/infoType_fields.md
The following infoType
values have been tested:
Name | infoType | Request Data | Recieve Data | Notes |
---|---|---|---|---|
Clean | 21005 | {“mode”:“smartClean”,“globalCleanTimes”:1} | None | |
Recharge | 21012 | {“cmd”:“start”} | None | |
Pause | 21017 | {“cmd”:“pause”} | None | |
Heartbeat | 21006 | {“heartbeatSec”:60} | None | Unsure what exactly it’s used for. Doesn’t appear to cause any issues if it isn’t sent. |
Set Clean Mode | 21022 | {“cmd”:“quiet”, “cleanType”:“total”} | None | cmd one of [“quiet”,“auto”,“strong”,“max”] |
Set LED mode | 21024 | {“cmd”:“setledswitch”,“value”:0} | None | |
Set Avoid Walls | 21024 | {“cmd”:“setSoftAlongWall”, “value”:1} | None | |
Find Robot | 21020 | {“ctrlCode”:3010} | None | |
Unknown 1 | 21011 | {“startPos”:0,“userId”:“0”,“mask”:0} | None | Unknown. Sent regularly during cleaning. |
Unknown 2 | 21015 | None | None | Sent once on boot. Unknown. Doesn’t appear to cause any issues if it isn’t sent. |
The following have not yet been tested, use at your own risk:
Name | infoType | Request Data | Recieve Data | Notes |
---|---|---|---|---|
setRemoteControlNet | 21037 | [str] | None | Possibly related to enabling the UDP interface |
reboot | 21024 | {“cmd”:“reboot”,“value”: [i2]} | None | Logs reboot task with specified delay in milliseconds |
getWifiInfo | 21019 | None | Unknown |
A full list of all known infoTypes is available in the infoType_fields.md file.
Command 30000
is unknown, may be related to “composite cmds”
Where “None” is specified as a data type, the following is expected:
1{
2 "errno": 0,
3 "errmsg": "Succeeded",
4 "data": {}
5}
A full list of error codes is available here: https://smart.360.cn/clean/errorInfo_us.json
Additional APIs
There are a number of additional endpoints available:
UDP API
There is a UDP RC API on port 8790. It is normally closed, and opened when RC mode is enabled. It operates similarly to the Main API. It is not yet known if normal infoType commands can be sent here; this could allow local control. All known commands use infoType 20120
Sending movement commands: {"infoType":21020,"data":{"ctrlCode":3013,"ctrlParams":{"speedV":0.000000,"speedW":0.000000}},"packId":27}
Reporting current position: {"message":"OK","infoType":21020,"x":-261,"y":-392,"angle":1410,"packId":27}
Unknown, possibly to close connection: {"infoType":21020,"data":{"ctrlCode":4000},"packId":194}
Reversing the android app indicates a generic UDP API that appears to be able to recieve other commands, but this isn’t supported on my vacuum.
Push API
There is another API at that appears to be used by the app for for higher-traffic uses including battery status and mapping data. As sending commmands was my primary goal this has not been explored. On my device it shows traffic to 101.198.193.215
, which does not reverse to a DNS name.
Might just be a proxy for data directly from the vac, stuff that the main API doesn’t care about. I would like to get status info to integrate with Home Assistant, so this API is probably next.
Here is an example of a decrypted packet:
1{"createTime":"1703612364","data":"{\"message\":\"ok\",\"infoType\":20001,\"data\":{\"allArea\":5943,\"allTime\":445962,\"autoBoost\":0,\"cleanArea\":3,\"cleanId\":\"361TY000003000000-1703612364\",\"cleanTime\":50,\"elec\":89,\"elecReal\":89,\"errorState\":[0],\"errorTime\":0,\"lastSubMode\":\"total\",\"led\":0,\"mode\":\"charge\",\"mopStatus\":0,\"phi\":-1953,\"pos\":[540,-483],\"reliable\":1,\"showSmartArea\":0,\"showSweepArea\":0,\"soft\":1,\"subMode\":\"null\",\"timeStamp\":1703612364,\"timerStatus\":10,\"vol\":9,\"windPower\":0,\"workNoisy\":\"quiet\"}}","event":4,"sn":"361TY000003000000","taskid":"1703612364234672"}
Decrypting the packets is non-trivial, you need to patch the application to dump the keys at runtime. The Push API uses a wonky AES implementation; you need to convert the key to ASCII hex, read it as UTF-8, and use the first 16 bytes as both the key and IV for AES-128 in CBC mode.
1byte[] decrypt(string: key, byte[]: data) {
2 byte[] k = key.getBytes("utf-8");
3 byte[] _k = new byte[16];
4
5 for (int i2 = 0; i2 < k.len && i2 < 16; i2++) {
6 _k[i2] = k[i2];
7 }
8
9 c = crypto.init(
10 "decrypt",
11 "aes-128-cbc",
12 key: _k,
13 iv: _k
14 );
15 return c.decrypt(data);
16}
Misuse of key as IV aside, decoding a hex string as utf-8
means every byte is the ASCII character for one of 0x0-0xF (4 bits), not 0x00-0xFF (8 bits) if decoded as hex
, so knowing that you can only make a key with these chars (eg. 0x616564376264...
) gives us a significantly reduced key strength of (drumroll please…):
16 characters * 4 bits = 64 bits
I don’t yet know how to initiate a connection to the push API server. It appears a seperate key is required.
Other
This is a list of endpoints which is used by the 360 mobile app to control 360 smart ai devices like 360 S5 / S6 / S7 vacuum cleaner.
Many of these were initially documented by @iMarkus, you can view their original list here.
- https://q.smart.360.cn/clean/cmd/send: See above
- https://q.smart.360.cn/clean/devuser/updateInfo:
- https://q.smart.360.cn/clean/record/statis:
Returns total lifetime cleaning statistics for a given serial number, and the human readable string (data.clues) used on the Cleaning Report page of the app.
1{ 2 "errno": 0, 3 "errmsg": "Succeeded", 4 "data": { 5 "cleanArea": 594, 6 "cleanCount": 22, 7 "cleanTime": 44175, 8 "clues": "The accumulated cleaning area is equivalent to ((4.1)) basketball courts, saving you ((12.3)) hours", 9 "mopArea": 0 10 } 11}
- https://q.smart.360.cn/clean/record/allStatis: Similar to
clean/record/statis
, just returns seconds instead of hours.1{ 2 "errno": 0, 3 "errmsg": "Succeeded", 4 "data": { 5 "allCleanArea": 5183, 6 "allCleanCount": 662, 7 "allCleanTime": 833558, 8 "allMopArea": 0, 9 "bindNum": 1 10 } 11}
- https://q.smart.360.cn/clean/record/getList: Returns an array of recent cleaning sessions, including time, modes, statistiscs and success. Indexed by
data.list[].cleanId
, which is a string in the format${Serial}-${Timestamp}
, (which appears to matchdata.list[].startTime
) - https://q.smart.360.cn/clean/record/recentlycleanlist: Returns monthly and weekly history of square meters cleaned. Used on the Cleaning Report page of the app.
- https://q.smart.360.cn/clean/record/getOne:
- https://q.smart.360.cn/clean/ad/applist:
- https://q.smart.360.cn/clean/dev/getMaterialStatus: Unknown. No data is sent or recieved, just “Succeeded”. Appears to be accessed once on first launch.
- https://smart.360.cn/clean/modelalias.json: Returns a list of all the vaccum models with some OTA and naming data. Has flags (ovLogReport and cnLogReport) that appear to be whether log reporting is enabled on overseas servers and domestic servers
- https://smart.360.cn/clean/errorInfo_us.json: No authentication required. Returns a list of error codes and localized (en_us) strings.
- https://q.smart.360.cn/clean/record/getBackupMapList: Mapping
- https://q.smart.360.cn/s3_file_new/iot-master-clean-online-pub1-bjyt: Mapping
Other APIs:
- https://smart.360.cn/clean/errorInfo_us.json
- https://smart.360.cn/clean/share_act.json
- https://smart.360.cn/clean_dev/list_en_us.json
- https://sdk.s.360.cn/ak/e995f98d56967d946471af29d7bf99f1.html?m2=8D7ACEB0-8EA1-4AD5-9339-34D01918FC4A
- https://p.s.360.cn/update/update.php
- https://ota3.jia.360.cn/upgrade/getNewVersion
- https://p.s.360.cn/pstat/plog.php: Statistics
- https://q.smart.360.cn/common/dev/GetList: Returns a list of vaccums assosciated with an account. Authentication required.
1{ 2 "errno": 0, 3 "errmsg": "Succeeded", 4 "data": { 5 "list": [ 6 { 7 "sn": "[redacted]", 8 "devType": 3, 9 "icon": "https://p.ssl.qhimg.com/t01047fcf6ee991aeb9.png", 10 "title": "Example Vacuum", 11 "hardware": "S7", 12 "pkcode": "", 13 "hwSn": "", 14 "version": "1.8.9", 15 "versionCode": 2976, 16 "role": 2, 17 "online": 1, 18 "support": "{\"carpetMode\":1,\"cleanTimesInTimer\":1,\"deleteMap\":1,\"led\":1,\"logReport\":1,\"maxBanArea30\":1,\"maxMode\":1,\"mop\":1,\"mopBanArea\":1,\"multiQuietHours\":1,\"reboot\":1,\"remoteControl\":1,\"remoteControlWithMap\":1,\"restoreMap\":1,\"roomSweep\":1,\"rotateMap\":1,\"rssi\":1,\"saveMap\":1,\"setRoomAttrib\":1,\"smartArea\":1,\"softAlongWall\":1,\"sweepAreaInTimer\":1,\"timedSweepMode\":1,\"virtualWall\":1,\"voicePacket\":1,\"volume\":1}", 19 "alexaDefault": 0, 20 "ownerQid": 0, 21 "ownerImage": "", 22 "ownerNickName": "[redacted]", 23 "bindTime": 0 24 } 25 ] 26 } 27}
- https://q.smart.360.cn/common/share/getInviteList
- https://q.smart.360.cn/common/share/inviteByCode
- https://q.smart.360.cn/common/share/getInviteList
Home Assistant
A minimal working home assistant config. Place the cookie contents in a variable called 360_token
in ./secret.yaml
1rest_command:
2 vacuum_pause_button:
3 url: "https://q.smart.360.cn/clean/cmd/send"
4 method: "post"
5 headers:
6 Content-Type: "application/x-www-form-urlencoded"
7 Host: q.smart.360.cn
8 Accept-Encoding: gzip
9 Cookie: !secret 360_token
10 payload: "sn=361TY000000000000&infoType=21017&data=%7B%22cmd%22%3A%22pause%22%7D&devType=3"
11 vacuum_charge_button:
12 url: "https://q.smart.360.cn/clean/cmd/send"
13 method: "post"
14 headers:
15 Content-Type: "application/x-www-form-urlencoded"
16 Host: q.smart.360.cn
17 Accept-Encoding: gzip
18 Cookie: !secret 360_token
19 payload: "sn=361TY000000000000&infoType=21012&data=%7B%22cmd%22%3A%22start%22%7D&devType=3"
20 vacuum_clean_button:
21 url: "https://q.smart.360.cn/clean/cmd/send"
22 method: "post"
23 headers:
24 Content-Type: "application/x-www-form-urlencoded"
25 Host: q.smart.360.cn
26 Accept-Encoding: gzip
27 Cookie: !secret 360_token
28 payload: "sn=361TY000000000000&infoType=21005&data=%7B%22mode%22%3A%22smartClean%22%2C%22globalCleanTimes%22%3A1%7D&devType=3"
29 vacuum_find_button:
30 url: "https://q.smart.360.cn/clean/cmd/send"
31 method: "post"
32 headers:
33 Content-Type: "application/x-www-form-urlencoded"
34 Host: q.smart.360.cn
35 Accept-Encoding: gzip
36 Cookie: !secret 360_token
37 payload: "sn=361TY000000000000&infoType=21020&data=%7B%22ctrlCode%22%3A3010%7D&devType=3"
Homebridge
The homebridge-http-switch
plugin is required. Adjust name/room as required, I find “Turn on Vaccum Cleaner” works great with Siri.
1{
2 "accessories": [
3 {
4 "accessory": "HTTP-SWITCH",
5 "name": "Vacuum Cleaner",
6 "switchType": "stateless",
7 "onUrl": {
8 "url": "https://q.smart.360.cn/clean/cmd/send",
9 "method": "POST",
10 "headers": {
11 "Content-Type": "application/x-www-form-urlencoded",
12 "Host": "q.smart.360.cn",
13 "Accept-Encoding": "gzip",
14 "Cookie": /Insert your cookie here/,
15 },
16 "body": "sn=361TY0000000000 &infoType=21005&data=%7B%22mode%22%3A%22smartClean%22%2C%22globalCleanTimes%22%3A1%7D&devType=3"
17 }
18 }
19 ],
20 "platforms": [],
21 "plugins": [
22 {
23 "platform": "homebridge-http-switch",
24 "name": "homebridge-http-switch"
25 }
26 ]
27}
Notes
There’s a few abbreviations used in their logging:
“jioe” is JSONIOException, “ste” is SocketTimeoutException, and
The package name com.qihoo.smarthome.sweeper.ui.o1
is RC mode, UDP reciever