stonegray's site

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

  1. Obtaining vacuum keys
  2. Sending commands
  3. Command list
  4. Home Assistant
  5. Homebridge
  6. Additional APIs
  7. Notes

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:

  1. 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
  2. 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.

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.

Other APIs:

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

References

https://github.com/iMarkus/360smartai

#hardware   #homeassistant   #reversing   #hacks   #long