homebridge-roborock-vacuum 2.2.7 → 2.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -1
- package/README.md +72 -55
- package/config.schema.json +3 -3
- package/homebridge-ui/public/boba.png +0 -0
- package/homebridge-ui/public/index.html +37 -3
- package/homebridge-ui/public/styles.css +52 -0
- package/package.json +26 -9
- package/roborockLib/data/roborock.clientID +0 -4
- package/roborockLib/lib/sniffing/mitmproxy_roborock.py +0 -300
- package/roborockLib/test.js +0 -46
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.8
|
|
4
|
+
|
|
5
|
+
- **New**: Project website in English, Traditional Chinese and Japanese: [tasict.github.io/homebridge-roborock-vacuum](https://tasict.github.io/homebridge-roborock-vacuum/). It is now the plugin's homepage in the Homebridge UI
|
|
6
|
+
- **New**: You can now support the plugin by buying me a boba (paid by card, no PayPal account needed) or with PayPal: from the bottom of the plugin settings, the donate link in the Homebridge UI, the README or GitHub's Sponsor button
|
|
7
|
+
- **Change (packaging)**: The npm package no longer contains a development test script, a leftover client ID file or the protocol sniffing helper; it now holds only what the plugin needs to run
|
|
8
|
+
- **Docs**: README rewritten around the current feature set — supported models (with corrected names such as Qrevo Curv), requirements matching the supported Homebridge and Node.js versions, the sign-in flow, and a reference of every config option. The header image is now the plugin's own icon
|
|
9
|
+
- **Change (config UI)**: The settings page describes what it is for, and has a small support footer below the settings
|
|
10
|
+
|
|
3
11
|
## 2.2.7
|
|
4
12
|
|
|
5
13
|
- **Fix (connectivity)**: The plugin no longer fails to start when the Roborock cloud is unreachable. Startup used to abandon everything after the home-data request timed out (`AxiosError: timeout of 20000ms exceeded`, `ECONNRESET`), so no accessories were registered and every command was refused with "Adapter not inited" — even though the vacuum was reachable on the LAN the whole time. The home data (device list and local keys) is now cached on disk, and when the cloud fetch fails the plugin starts in **local-only mode** from that cache: it still runs LAN discovery, connects to each vacuum over the local protocol, publishes the accessories, and accepts start/stop/dock commands. Cloud-only features (scenes, push updates, fresh online flags) resume by themselves once the cloud answers again, without a Homebridge restart. One successful online start is needed to seed the cache; with no cache the plugin stays idle as before, now with a message that says why
|
|
@@ -55,7 +63,7 @@ Maintenance release — no functional changes to device control. Reworks the fou
|
|
|
55
63
|
|
|
56
64
|
## 2.2.0
|
|
57
65
|
|
|
58
|
-
Connectivity and device-support release, porting recent fixes from the python-roborock and ioBroker.roborock upstream projects
|
|
66
|
+
Connectivity and device-support release, porting recent fixes from the python-roborock and ioBroker.roborock upstream projects.
|
|
59
67
|
|
|
60
68
|
- **New Feature**: Docks introduced since 2025 (Qrevo Curv, Saros 10, Qrevo S5V, Saros 20, and other new dock type codes 10–40) now get their dust-collection, mop-wash, and drying features — including the dock mop-wash switch. Unknown future dock types default to full-featured instead of featureless
|
|
61
69
|
- **New Feature**: Room names now resolve for vacuums shared from another Roborock account (fetched via the device-share rooms API), so Matter room cleaning and the current-room publisher work on shared devices
|
package/README.md
CHANGED
|
@@ -1,82 +1,88 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/tasict/homebridge-roborock-vacuum/master/branding/icon.png" width="96" height="96" alt="">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
# Homebridge Roborock Vacuum
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
**Website: [tasict.github.io/homebridge-roborock-vacuum](https://tasict.github.io/homebridge-roborock-vacuum/)** (English, 繁體中文, 日本語)
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
[](https://www.npmjs.com/package/homebridge-roborock-vacuum)
|
|
10
|
+
[](https://www.npmjs.com/package/homebridge-roborock-vacuum)
|
|
11
|
+
[](LICENSE)
|
|
8
12
|
|
|
9
|
-
`homebridge-roborock-vacuum` brings
|
|
13
|
+
`homebridge-roborock-vacuum` is a [Homebridge](https://homebridge.io) plugin that brings Roborock robot vacuums into Apple Home, over HomeKit or Matter. Sign in with your Roborock app account and the plugin finds your vacuums by itself; you can then start and stop them from the Home app, with Siri or from automations. Commands go to the vacuum over your home network, so it stays controllable when the Roborock cloud is down.
|
|
14
|
+
|
|
15
|
+
Free and open source. If it saves you a few chores, you can [buy me a boba](https://tasict.bobaboba.me) (by card, no PayPal account needed) or [tip with PayPal](https://paypal.me/tasict).
|
|
10
16
|
|
|
11
17
|
This plugin is inspired by and adapted from the [ioBroker.roborock](https://github.com/copystring/ioBroker.roborock) project by copystring.
|
|
12
18
|
|
|
13
19
|
## Features
|
|
14
20
|
|
|
15
|
-
- **Automatic
|
|
16
|
-
- **Start
|
|
17
|
-
- **
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
Roborock
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
Roborock S6 Pure
|
|
26
|
-
Roborock S6 MaxV
|
|
27
|
-
Roborock S7
|
|
28
|
-
Roborock S7 MaxV (Ultra)
|
|
29
|
-
Roborock Q5+
|
|
30
|
-
Roborock Q7
|
|
31
|
-
Roborock Q7 Max
|
|
32
|
-
Roborock S7 Pro Ultra
|
|
33
|
-
Roborock S7 Max Ultra
|
|
34
|
-
Roborock S8
|
|
35
|
-
Roborock S8 Pro Ultra
|
|
36
|
-
Roborock Q Revo
|
|
37
|
-
Roborock Q8 Max
|
|
38
|
-
Roborock Q5 Pro
|
|
39
|
-
Roborock Q Revo Pro
|
|
40
|
-
Roborock Qrevo S
|
|
41
|
-
Roborock Qrevo Curve
|
|
42
|
-
Roborock Saros 10
|
|
43
|
-
Roborock (roborock.vacuum.a95)
|
|
44
|
-
Roborock (roborock.vacuum.a159)
|
|
45
|
-
Roborock (roborock.vacuum.ss07)
|
|
21
|
+
- **Automatic device discovery** — no device IDs or IP addresses to look up. The Roborock region is detected from your account.
|
|
22
|
+
- **Start, stop and send home** — over HomeKit the vacuum is a fan-style accessory: turn it on to start cleaning, off to stop and return to the dock.
|
|
23
|
+
- **Battery, charging and status** — battery level, charging state, low-battery warning, and a fault/offline flag when the vacuum reports an error or loses its connection.
|
|
24
|
+
- **Roborock scenes as switches** — every enabled scene from the Roborock app that uses the vacuum can be run from Apple Home.
|
|
25
|
+
- **Dock mop-wash switch** on docks that wash the mop. See [Dock mop-wash switch](#dock-mop-wash-switch).
|
|
26
|
+
- **Consumables** (HomeKit) — remaining life of the filter, main brush and side brush, with a change reminder at 5% or less.
|
|
27
|
+
- **Matter (Beta)** — publish a vacuum as a native Matter robot vacuum, with room cleaning and clean modes. See [Matter support (Beta)](#matter-support-beta).
|
|
28
|
+
- **Works through cloud outages** — local control over the LAN, and a local-only start when the Roborock cloud is unreachable. See [Cloud outages and local-only mode](#cloud-outages-and-local-only-mode).
|
|
29
|
+
- **Current room → MQTT** (optional) — publish the room being cleaned to your MQTT broker. See [Current Room → MQTT](#current-room--mqtt-optional-telemetry).
|
|
30
|
+
- **Per-device choice** — publish each vacuum over HomeKit or Matter, or skip it (for example a vacuum shared from another home).
|
|
46
31
|
|
|
47
|
-
##
|
|
32
|
+
## Supported vacuums
|
|
48
33
|
|
|
49
|
-
|
|
34
|
+
The plugin works with vacuums that are on your **Roborock app** account. These models are known to work:
|
|
50
35
|
|
|
51
|
-
|
|
52
|
-
- A Roborock app account (email and password).
|
|
53
|
-
- [Homebridge](https://github.com/homebridge/homebridge) installed on a server (e.g., Raspberry Pi).
|
|
54
|
-
- Node.js and npm installed on your Homebridge server.
|
|
36
|
+
S4, S4 Max, S5 Max, S6, S6 Pure, S6 MaxV, S7, S7 MaxV (Ultra), S7 Pro Ultra, S7 Max Ultra, S8, S8 Pro Ultra, Q5+, Q5 Pro, Q7, Q7 Max, Q8 Max, Qrevo, Qrevo Pro, Qrevo S, Qrevo Curv, Saros 10, and the models `roborock.vacuum.a95`, `roborock.vacuum.a159` and `roborock.vacuum.ss07`.
|
|
55
37
|
|
|
56
|
-
|
|
38
|
+
Most other vacuums in the Roborock app work as well; which extras (dock mop wash, consumables, clean modes) appear depends on what the model reports. A vacuum that is set up only in the Xiaomi Home app is not on your Roborock account, so the plugin cannot see it.
|
|
39
|
+
|
|
40
|
+
## Requirements
|
|
41
|
+
|
|
42
|
+
- A Roborock vacuum added to the Roborock app, and the email address of that Roborock account.
|
|
43
|
+
- [Homebridge](https://homebridge.io) 1.6 or later, on Node.js 18.20, 20.15, 22 or 24.
|
|
44
|
+
- For Matter: Homebridge 2.x with Matter enabled on the bridge the plugin runs on.
|
|
57
45
|
|
|
58
|
-
|
|
46
|
+
## Installation
|
|
59
47
|
|
|
60
|
-
|
|
48
|
+
In the Homebridge UI, open **Plugins**, search for **Roborock Vacuum** and install `homebridge-roborock-vacuum`.
|
|
61
49
|
|
|
62
|
-
|
|
50
|
+
Without the UI, install it next to Homebridge:
|
|
63
51
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
npm install -g homebridge-roborock-vacuum
|
|
68
|
-
```
|
|
52
|
+
```bash
|
|
53
|
+
npm install -g homebridge-roborock-vacuum
|
|
54
|
+
```
|
|
69
55
|
|
|
70
56
|
## Configuration
|
|
71
57
|
|
|
72
|
-
|
|
58
|
+
Configure the plugin from its settings page in the Homebridge UI:
|
|
73
59
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
- **
|
|
60
|
+
1. **Sign in.** Enter your Roborock app email. Either enter your password and press **Password Login**, or press **Send 2FA Email** and enter the code from the email with **Verify Code** (also the way to finish a login that asks for two-factor authentication). Leave **Region** on _Auto detect_ unless detection picks the wrong server. The settings page stores an encrypted session token; you do not need to sign in again after a restart.
|
|
61
|
+
2. **Choose a protocol per vacuum.** Press **Load devices** to list the vacuums on your account and pick one for each:
|
|
62
|
+
- **HomeKit (HAP)** — the default. The vacuum is bridged to Apple Home as a fan-style accessory, as in previous versions.
|
|
63
|
+
- **Matter (Beta)** — the vacuum is published over Matter as a robotic vacuum cleaner, so it gets the native vacuum icon and controls in Apple Home (and works with Google Home, Alexa and SmartThings). This is an experimental feature — read the [Matter support (Beta)](#matter-support-beta) section below before enabling it.
|
|
64
|
+
- **Skip** — the vacuum is excluded entirely. You can also enter device IDs manually in the list below; manual entries are always skipped.
|
|
65
|
+
3. **Restart Homebridge.**
|
|
77
66
|
|
|
78
67
|
When Homebridge restarts, each device is (re)published according to its selection. A device that was cached under a different protocol (or is now skipped) is removed from the old bridge first. **Changing a device's protocol re-creates the accessory**, so it loses its room and automation assignments in your home app and must be re-organized there.
|
|
79
68
|
|
|
69
|
+
### Options
|
|
70
|
+
|
|
71
|
+
The settings page writes these keys to the `RoborockVacuumPlatform` block in `config.json`:
|
|
72
|
+
|
|
73
|
+
| Key | Description |
|
|
74
|
+
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
75
|
+
| `email` | Roborock app account email. Required. |
|
|
76
|
+
| `password` | Roborock account password, saved by **Password Login**. Not needed when you sign in with an email code. |
|
|
77
|
+
| `encryptedToken` | Encrypted session data, managed by the settings page. |
|
|
78
|
+
| `baseURL` | Roborock API endpoint. Leave empty to auto-detect; otherwise `https://usiot.roborock.com` (US), `https://euiot.roborock.com` (EU), `https://cniot.roborock.com` (CN) or `https://api.roborock.com` (Asia). |
|
|
79
|
+
| `language` | Language of the names the plugin gives to switches and sensors it adds (`en`, `de`, `es`, `fr`, `it`, `nl`, `pl`, `pt`, `ru`, `uk`, `zh-cn`, `zh-tw`). Default `en`. |
|
|
80
|
+
| `skipDevices` | Device IDs to leave out, as a list or a comma- or newline-separated string. |
|
|
81
|
+
| `matterDevices` | Device IDs to publish over Matter instead of HomeKit. Ignored without Homebridge 2 and Matter. |
|
|
82
|
+
| `matterSceneDevices` | Matter device IDs whose Roborock scenes become Matter buttons. When omitted, every Matter device gets them. |
|
|
83
|
+
| `currentRoomMqtt` | Optional current-room telemetry, see [Current Room → MQTT](#current-room--mqtt-optional-telemetry). |
|
|
84
|
+
| `debugMode` | Write debug messages to the Homebridge log. Default `false`. |
|
|
85
|
+
|
|
80
86
|
### Matter support (Beta)
|
|
81
87
|
|
|
82
88
|
> ⚠️ **This is an experimental, lab-grade beta feature.** Matter support for robotic vacuums is still very new on the controller side. In particular, **Apple Home's compatibility is currently poor and not very stable**: the bridge is uncertified (Apple shows an "Uncertified Accessory" warning), the accessory appears as a generic **"Matter Accessory"** during pairing and must be named manually, room selection needs iOS 18.4 or later, and accessories can occasionally show "Not responding" or lose features after iOS updates. If you want the most reliable day-to-day experience, stay on **HomeKit (HAP)**. Choose Matter only if you want the native vacuum UI and room cleaning, and can live with rough edges.
|
|
@@ -191,3 +197,14 @@ A retained JSON message is published whenever the room changes:
|
|
|
191
197
|
- Room names are resolved from the robot's saved map; name your rooms in the Roborock app for them to appear.
|
|
192
198
|
|
|
193
199
|
> Validated on a Roborock Qrevo (`roborock.vacuum.a185`). On models that don't populate `cleaning_info`, the payload degrades gracefully to `segment_id: -1` / `room: null`.
|
|
200
|
+
|
|
201
|
+
## Support
|
|
202
|
+
|
|
203
|
+
If this plugin is useful to you, I'd love it if you bought me a boba:
|
|
204
|
+
|
|
205
|
+
- **[Buy me a boba](https://tasict.bobaboba.me)**: pay by card, no PayPal account needed
|
|
206
|
+
- **[Tip with PayPal](https://paypal.me/tasict)**
|
|
207
|
+
|
|
208
|
+
## Trademarks
|
|
209
|
+
|
|
210
|
+
Roborock is a trademark of Beijing Roborock Technology Co., Ltd. Apple Home and HomeKit are trademarks of Apple Inc. Matter is a trademark of the Connectivity Standards Alliance. This plugin is unofficial and is not affiliated with, endorsed by, or sponsored by any of them.
|
package/config.schema.json
CHANGED
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
"title": "Email",
|
|
18
18
|
"type": "string",
|
|
19
19
|
"format": "email",
|
|
20
|
-
"description": "Roborock account
|
|
20
|
+
"description": "Email of your Roborock app account"
|
|
21
21
|
},
|
|
22
22
|
"password": {
|
|
23
23
|
"title": "Password",
|
|
24
24
|
"type": "string",
|
|
25
25
|
"placeholder": "Password",
|
|
26
|
-
"description": "Roborock account password"
|
|
26
|
+
"description": "Roborock account password (not needed when signing in with an email code)"
|
|
27
27
|
},
|
|
28
28
|
"encryptedToken": {
|
|
29
29
|
"title": "Encrypted Token",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"skipDevices": {
|
|
35
35
|
"title": "Skipped Device IDs",
|
|
36
36
|
"type": "string",
|
|
37
|
-
"description": "Roborock device IDs to
|
|
37
|
+
"description": "Roborock device IDs to leave out of Apple Home (neither HomeKit nor Matter)."
|
|
38
38
|
},
|
|
39
39
|
"matterDevices": {
|
|
40
40
|
"title": "Matter Device IDs",
|
|
Binary file
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
<link rel="stylesheet" href="styles.css?v=2.
|
|
1
|
+
<link rel="stylesheet" href="styles.css?v=2.2.8" />
|
|
2
2
|
|
|
3
3
|
<main class="rr-container">
|
|
4
4
|
<header>
|
|
5
5
|
<h1>Roborock Vacuum</h1>
|
|
6
6
|
<p class="subtitle">
|
|
7
|
-
|
|
7
|
+
Sign in to your Roborock account and choose how each vacuum appears in
|
|
8
|
+
Apple Home.
|
|
8
9
|
</p>
|
|
9
10
|
</header>
|
|
10
11
|
|
|
@@ -202,8 +203,41 @@
|
|
|
202
203
|
</div>
|
|
203
204
|
</div>
|
|
204
205
|
</section>
|
|
206
|
+
|
|
207
|
+
<footer class="rr-support">
|
|
208
|
+
<p class="rr-support-text">
|
|
209
|
+
This plugin is free and open source. If it saves you a few chores, you can
|
|
210
|
+
buy me a boba or send a tip with PayPal.
|
|
211
|
+
</p>
|
|
212
|
+
<div class="rr-support-links">
|
|
213
|
+
<a
|
|
214
|
+
class="rr-tip"
|
|
215
|
+
href="https://tasict.bobaboba.me"
|
|
216
|
+
target="_blank"
|
|
217
|
+
rel="noopener noreferrer"
|
|
218
|
+
><img src="boba.png" alt="" width="20" height="20" />Buy me a boba</a
|
|
219
|
+
>
|
|
220
|
+
<a
|
|
221
|
+
class="rr-tip"
|
|
222
|
+
href="https://paypal.me/tasict"
|
|
223
|
+
target="_blank"
|
|
224
|
+
rel="noopener noreferrer"
|
|
225
|
+
>Tip with PayPal</a
|
|
226
|
+
>
|
|
227
|
+
<a
|
|
228
|
+
class="rr-tip"
|
|
229
|
+
href="https://tasict.github.io/homebridge-roborock-vacuum/"
|
|
230
|
+
target="_blank"
|
|
231
|
+
rel="noopener noreferrer"
|
|
232
|
+
>Plugin website</a
|
|
233
|
+
>
|
|
234
|
+
</div>
|
|
235
|
+
<p class="rr-support-note">
|
|
236
|
+
Boba is paid by card. No PayPal account needed.
|
|
237
|
+
</p>
|
|
238
|
+
</footer>
|
|
205
239
|
</main>
|
|
206
240
|
|
|
207
241
|
<div id="toast-container" class="rr-toast-container"></div>
|
|
208
242
|
|
|
209
|
-
<script src="index.js?v=2.
|
|
243
|
+
<script src="index.js?v=2.2.8"></script>
|
|
@@ -303,3 +303,55 @@ body.dark-mode .rr-toast {
|
|
|
303
303
|
opacity: 1;
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
|
+
|
|
307
|
+
/* Support: a quiet footer below the settings, never in the way of a task. */
|
|
308
|
+
.rr-support {
|
|
309
|
+
margin-top: 4px;
|
|
310
|
+
padding-top: 14px;
|
|
311
|
+
border-top: 1px solid var(--rr-border);
|
|
312
|
+
font-size: 0.8rem;
|
|
313
|
+
}
|
|
314
|
+
.rr-support-text {
|
|
315
|
+
margin: 0 0 8px;
|
|
316
|
+
color: var(--rr-muted);
|
|
317
|
+
line-height: 1.5;
|
|
318
|
+
}
|
|
319
|
+
.rr-support-links {
|
|
320
|
+
display: flex;
|
|
321
|
+
flex-wrap: wrap;
|
|
322
|
+
gap: 8px;
|
|
323
|
+
}
|
|
324
|
+
.rr-tip {
|
|
325
|
+
display: inline-flex;
|
|
326
|
+
align-items: center;
|
|
327
|
+
gap: 6px;
|
|
328
|
+
min-height: 30px;
|
|
329
|
+
padding: 3px 12px;
|
|
330
|
+
border: 1px solid var(--rr-border);
|
|
331
|
+
border-radius: 8px;
|
|
332
|
+
color: inherit;
|
|
333
|
+
font-weight: 600;
|
|
334
|
+
text-decoration: none;
|
|
335
|
+
white-space: nowrap;
|
|
336
|
+
}
|
|
337
|
+
.rr-tip:hover,
|
|
338
|
+
.rr-tip:focus {
|
|
339
|
+
color: inherit;
|
|
340
|
+
text-decoration: none;
|
|
341
|
+
border-color: var(--rr-muted);
|
|
342
|
+
}
|
|
343
|
+
.rr-tip:focus-visible {
|
|
344
|
+
outline: 2px solid #0d6efd;
|
|
345
|
+
outline-offset: 2px;
|
|
346
|
+
}
|
|
347
|
+
.rr-tip img {
|
|
348
|
+
width: 20px;
|
|
349
|
+
height: 20px;
|
|
350
|
+
margin-left: -5px;
|
|
351
|
+
border-radius: 5px;
|
|
352
|
+
}
|
|
353
|
+
.rr-support-note {
|
|
354
|
+
margin: 8px 0 0;
|
|
355
|
+
color: var(--rr-muted);
|
|
356
|
+
font-size: 0.75rem;
|
|
357
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-roborock-vacuum",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
3
|
+
"displayName": "Homebridge Roborock Vacuum",
|
|
4
|
+
"version": "2.2.8",
|
|
5
|
+
"description": "Homebridge plugin that brings Roborock robot vacuums to Apple Home over HomeKit or Matter, with local control on your network.",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"keywords": [
|
|
8
|
+
"homebridge",
|
|
7
9
|
"homebridge-plugin",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
+
"homekit",
|
|
11
|
+
"matter",
|
|
12
|
+
"roborock",
|
|
10
13
|
"roborock vacuum",
|
|
14
|
+
"robot vacuum",
|
|
15
|
+
"vacuum cleaner",
|
|
11
16
|
"supports-hap",
|
|
12
|
-
"
|
|
13
|
-
"roborock",
|
|
14
|
-
"homekit"
|
|
17
|
+
"supports-matter"
|
|
15
18
|
],
|
|
16
19
|
"repository": {
|
|
17
20
|
"type": "git",
|
|
@@ -20,14 +23,28 @@
|
|
|
20
23
|
"bugs": {
|
|
21
24
|
"url": "https://github.com/tasict/homebridge-roborock-vacuum/issues"
|
|
22
25
|
},
|
|
23
|
-
"homepage": "https://github.
|
|
26
|
+
"homepage": "https://tasict.github.io/homebridge-roborock-vacuum/",
|
|
27
|
+
"funding": [
|
|
28
|
+
{
|
|
29
|
+
"type": "individual",
|
|
30
|
+
"url": "https://tasict.bobaboba.me"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"type": "paypal",
|
|
34
|
+
"url": "https://paypal.me/tasict"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
24
37
|
"main": "dist/index.js",
|
|
25
38
|
"files": [
|
|
26
39
|
"CHANGELOG.md",
|
|
27
40
|
"config.schema.json",
|
|
28
41
|
"dist",
|
|
42
|
+
"!dist/**/*.test.*",
|
|
29
43
|
"homebridge-ui",
|
|
30
|
-
"roborockLib"
|
|
44
|
+
"roborockLib",
|
|
45
|
+
"!roborockLib/data",
|
|
46
|
+
"!roborockLib/lib/sniffing",
|
|
47
|
+
"!roborockLib/test.js"
|
|
31
48
|
],
|
|
32
49
|
"engines": {
|
|
33
50
|
"homebridge": "^1.6.0 || ^2.0.0-beta.0",
|
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import sys
|
|
3
|
-
|
|
4
|
-
from mitmproxy.utils import strutils
|
|
5
|
-
from mitmproxy import ctx
|
|
6
|
-
from mitmproxy import tcp
|
|
7
|
-
from websocket import create_connection
|
|
8
|
-
import json
|
|
9
|
-
|
|
10
|
-
import struct
|
|
11
|
-
import binascii
|
|
12
|
-
from struct import unpack
|
|
13
|
-
|
|
14
|
-
ws = create_connection("ws://localhost:7906")
|
|
15
|
-
|
|
16
|
-
class MQTTControlPacket:
|
|
17
|
-
# Packet types
|
|
18
|
-
(
|
|
19
|
-
CONNECT,
|
|
20
|
-
CONNACK,
|
|
21
|
-
PUBLISH,
|
|
22
|
-
PUBACK,
|
|
23
|
-
PUBREC,
|
|
24
|
-
PUBREL,
|
|
25
|
-
PUBCOMP,
|
|
26
|
-
SUBSCRIBE,
|
|
27
|
-
SUBACK,
|
|
28
|
-
UNSUBSCRIBE,
|
|
29
|
-
UNSUBACK,
|
|
30
|
-
PINGREQ,
|
|
31
|
-
PINGRESP,
|
|
32
|
-
DISCONNECT,
|
|
33
|
-
) = range(1, 15)
|
|
34
|
-
|
|
35
|
-
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Table_2.1_-
|
|
36
|
-
Names = [
|
|
37
|
-
"reserved",
|
|
38
|
-
"CONNECT",
|
|
39
|
-
"CONNACK",
|
|
40
|
-
"PUBLISH",
|
|
41
|
-
"PUBACK",
|
|
42
|
-
"PUBREC",
|
|
43
|
-
"PUBREL",
|
|
44
|
-
"PUBCOMP",
|
|
45
|
-
"SUBSCRIBE",
|
|
46
|
-
"SUBACK",
|
|
47
|
-
"UNSUBSCRIBE",
|
|
48
|
-
"UNSUBACK",
|
|
49
|
-
"PINGREQ",
|
|
50
|
-
"PINGRESP",
|
|
51
|
-
"DISCONNECT",
|
|
52
|
-
"reserved",
|
|
53
|
-
]
|
|
54
|
-
|
|
55
|
-
PACKETS_WITH_IDENTIFIER = [
|
|
56
|
-
PUBACK,
|
|
57
|
-
PUBREC,
|
|
58
|
-
PUBREL,
|
|
59
|
-
PUBCOMP,
|
|
60
|
-
SUBSCRIBE,
|
|
61
|
-
SUBACK,
|
|
62
|
-
UNSUBSCRIBE,
|
|
63
|
-
UNSUBACK,
|
|
64
|
-
]
|
|
65
|
-
|
|
66
|
-
def __init__(self, packet):
|
|
67
|
-
self._packet = packet
|
|
68
|
-
|
|
69
|
-
print(sys.executable)
|
|
70
|
-
# Fixed header
|
|
71
|
-
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718020
|
|
72
|
-
self.packet_type = self._parse_packet_type()
|
|
73
|
-
self.packet_type_human = self.Names[self.packet_type]
|
|
74
|
-
self.dup, self.qos, self.retain = self._parse_flags()
|
|
75
|
-
self.remaining_length = self._parse_remaining_length()
|
|
76
|
-
# Variable header & Payload
|
|
77
|
-
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718024
|
|
78
|
-
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718026
|
|
79
|
-
if self.packet_type == self.CONNECT:
|
|
80
|
-
self._parse_connect_variable_headers()
|
|
81
|
-
self._parse_connect_payload()
|
|
82
|
-
elif self.packet_type == self.PUBLISH:
|
|
83
|
-
self._parse_publish_variable_headers()
|
|
84
|
-
self._parse_publish_payload()
|
|
85
|
-
elif self.packet_type == self.SUBSCRIBE:
|
|
86
|
-
self._parse_subscribe_variable_headers()
|
|
87
|
-
self._parse_subscribe_payload()
|
|
88
|
-
elif self.packet_type == self.SUBACK:
|
|
89
|
-
pass
|
|
90
|
-
elif self.packet_type == self.UNSUBSCRIBE:
|
|
91
|
-
pass
|
|
92
|
-
else:
|
|
93
|
-
self.payload = None
|
|
94
|
-
|
|
95
|
-
def pprint(self):
|
|
96
|
-
s = f"[{self.Names[self.packet_type]}]"
|
|
97
|
-
|
|
98
|
-
if self.packet_type == self.CONNECT:
|
|
99
|
-
s += f"""
|
|
100
|
-
|
|
101
|
-
Client Id: {self.payload['ClientId']}
|
|
102
|
-
Will Topic: {self.payload.get('WillTopic')}
|
|
103
|
-
Will Message: {strutils.bytes_to_escaped_str(self.payload.get('WillMessage', b'None'))}
|
|
104
|
-
User Name TEST: {self.payload.get('UserName')}
|
|
105
|
-
Password: {strutils.bytes_to_escaped_str(self.payload.get('Password', b'None'))}
|
|
106
|
-
"""
|
|
107
|
-
elif self.packet_type == self.SUBSCRIBE:
|
|
108
|
-
s += " sent topic filters: "
|
|
109
|
-
s += ", ".join([f"'{tf}'" for tf in self.topic_filters])
|
|
110
|
-
elif self.packet_type == self.PUBLISH:
|
|
111
|
-
topic_name = strutils.bytes_to_escaped_str(self.topic_name)
|
|
112
|
-
payload_hex = binascii.hexlify(self.payload).decode()
|
|
113
|
-
|
|
114
|
-
protocol = unpack('>H', self.payload[15:17])[0]
|
|
115
|
-
|
|
116
|
-
if (protocol == 101):
|
|
117
|
-
s += f" '{payload_hex}' to topic '{topic_name}'"
|
|
118
|
-
elif self.packet_type in [self.PINGREQ, self.PINGRESP]:
|
|
119
|
-
pass
|
|
120
|
-
else:
|
|
121
|
-
s = f"Packet type {self.Names[self.packet_type]} is not supported yet!"
|
|
122
|
-
|
|
123
|
-
return s
|
|
124
|
-
|
|
125
|
-
def _parse_length_prefixed_bytes(self, offset):
|
|
126
|
-
field_length_bytes = self._packet[offset : offset + 2]
|
|
127
|
-
field_length = struct.unpack("!H", field_length_bytes)[0]
|
|
128
|
-
|
|
129
|
-
field_content_bytes = self._packet[offset + 2 : offset + 2 + field_length]
|
|
130
|
-
|
|
131
|
-
return field_length + 2, field_content_bytes
|
|
132
|
-
|
|
133
|
-
def _parse_publish_variable_headers(self):
|
|
134
|
-
offset = len(self._packet) - self.remaining_length
|
|
135
|
-
|
|
136
|
-
field_length, field_content_bytes = self._parse_length_prefixed_bytes(offset)
|
|
137
|
-
self.topic_name = field_content_bytes
|
|
138
|
-
|
|
139
|
-
if self.qos in [0x01, 0x02]:
|
|
140
|
-
offset += field_length
|
|
141
|
-
self.packet_identifier = self._packet[offset : offset + 2]
|
|
142
|
-
|
|
143
|
-
def _parse_publish_payload(self):
|
|
144
|
-
fixed_header_length = len(self._packet) - self.remaining_length
|
|
145
|
-
variable_header_length = 2 + len(self.topic_name)
|
|
146
|
-
|
|
147
|
-
if self.qos in [0x01, 0x02]:
|
|
148
|
-
variable_header_length += 2
|
|
149
|
-
|
|
150
|
-
offset = fixed_header_length + variable_header_length
|
|
151
|
-
|
|
152
|
-
self.payload = self._packet[offset:]
|
|
153
|
-
|
|
154
|
-
def _parse_subscribe_variable_headers(self):
|
|
155
|
-
self._parse_packet_identifier()
|
|
156
|
-
|
|
157
|
-
def _parse_subscribe_payload(self):
|
|
158
|
-
offset = len(self._packet) - self.remaining_length + 2
|
|
159
|
-
|
|
160
|
-
self.topic_filters = {}
|
|
161
|
-
|
|
162
|
-
while len(self._packet) - offset > 0:
|
|
163
|
-
field_length, topic_filter_bytes = self._parse_length_prefixed_bytes(offset)
|
|
164
|
-
offset += field_length
|
|
165
|
-
|
|
166
|
-
qos = self._packet[offset : offset + 1]
|
|
167
|
-
offset += 1
|
|
168
|
-
|
|
169
|
-
topic_filter = topic_filter_bytes.decode("utf-8")
|
|
170
|
-
self.topic_filters[topic_filter] = {"qos": qos}
|
|
171
|
-
|
|
172
|
-
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718030
|
|
173
|
-
def _parse_connect_variable_headers(self):
|
|
174
|
-
offset = len(self._packet) - self.remaining_length
|
|
175
|
-
|
|
176
|
-
self.variable_headers = {}
|
|
177
|
-
self.connect_flags = {}
|
|
178
|
-
|
|
179
|
-
self.variable_headers["ProtocolName"] = self._packet[offset : offset + 6]
|
|
180
|
-
self.variable_headers["ProtocolLevel"] = self._packet[offset + 6 : offset + 7]
|
|
181
|
-
self.variable_headers["ConnectFlags"] = self._packet[offset + 7 : offset + 8]
|
|
182
|
-
self.variable_headers["KeepAlive"] = self._packet[offset + 8 : offset + 10]
|
|
183
|
-
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc385349229
|
|
184
|
-
self.connect_flags["CleanSession"] = bool(
|
|
185
|
-
self.variable_headers["ConnectFlags"][0] & 0x02
|
|
186
|
-
)
|
|
187
|
-
self.connect_flags["Will"] = bool(
|
|
188
|
-
self.variable_headers["ConnectFlags"][0] & 0x04
|
|
189
|
-
)
|
|
190
|
-
self.will_qos = (self.variable_headers["ConnectFlags"][0] >> 3) & 0x03
|
|
191
|
-
self.connect_flags["WillRetain"] = bool(
|
|
192
|
-
self.variable_headers["ConnectFlags"][0] & 0x20
|
|
193
|
-
)
|
|
194
|
-
self.connect_flags["Password"] = bool(
|
|
195
|
-
self.variable_headers["ConnectFlags"][0] & 0x40
|
|
196
|
-
)
|
|
197
|
-
self.connect_flags["UserName"] = bool(
|
|
198
|
-
self.variable_headers["ConnectFlags"][0] & 0x80
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718031
|
|
202
|
-
def _parse_connect_payload(self):
|
|
203
|
-
fields = []
|
|
204
|
-
offset = len(self._packet) - self.remaining_length + 10
|
|
205
|
-
|
|
206
|
-
while len(self._packet) - offset > 0:
|
|
207
|
-
field_length, field_content = self._parse_length_prefixed_bytes(offset)
|
|
208
|
-
fields.append(field_content)
|
|
209
|
-
offset += field_length
|
|
210
|
-
|
|
211
|
-
self.payload = {}
|
|
212
|
-
|
|
213
|
-
for f in fields:
|
|
214
|
-
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc385349242
|
|
215
|
-
if "ClientId" not in self.payload:
|
|
216
|
-
self.payload["ClientId"] = f.decode("utf-8")
|
|
217
|
-
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc385349243
|
|
218
|
-
elif self.connect_flags["Will"] and "WillTopic" not in self.payload:
|
|
219
|
-
self.payload["WillTopic"] = f.decode("utf-8")
|
|
220
|
-
elif self.connect_flags["Will"] and "WillMessage" not in self.payload:
|
|
221
|
-
self.payload["WillMessage"] = f
|
|
222
|
-
elif self.connect_flags["UserName"] and "UserName" not in self.payload:
|
|
223
|
-
self.payload["UserName"] = f.decode("utf-8")
|
|
224
|
-
elif self.connect_flags["Password"] and "Password" not in self.payload:
|
|
225
|
-
self.payload["Password"] = f
|
|
226
|
-
else:
|
|
227
|
-
raise Exception("")
|
|
228
|
-
|
|
229
|
-
def _parse_packet_type(self):
|
|
230
|
-
return self._packet[0] >> 4
|
|
231
|
-
|
|
232
|
-
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718022
|
|
233
|
-
def _parse_flags(self):
|
|
234
|
-
dup = None
|
|
235
|
-
qos = None
|
|
236
|
-
retain = None
|
|
237
|
-
|
|
238
|
-
if self.packet_type == self.PUBLISH:
|
|
239
|
-
dup = (self._packet[0] >> 3) & 0x01
|
|
240
|
-
qos = (self._packet[0] >> 1) & 0x03
|
|
241
|
-
retain = self._packet[0] & 0x01
|
|
242
|
-
|
|
243
|
-
return dup, qos, retain
|
|
244
|
-
|
|
245
|
-
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Table_2.4_Size
|
|
246
|
-
def _parse_remaining_length(self):
|
|
247
|
-
multiplier = 1
|
|
248
|
-
value = 0
|
|
249
|
-
i = 1
|
|
250
|
-
|
|
251
|
-
while True:
|
|
252
|
-
encodedByte = self._packet[i]
|
|
253
|
-
value += (encodedByte & 127) * multiplier
|
|
254
|
-
multiplier *= 128
|
|
255
|
-
|
|
256
|
-
if multiplier > 128 * 128 * 128:
|
|
257
|
-
raise Exception("Malformed Remaining Length")
|
|
258
|
-
|
|
259
|
-
if encodedByte & 128 == 0:
|
|
260
|
-
break
|
|
261
|
-
|
|
262
|
-
i += 1
|
|
263
|
-
|
|
264
|
-
return value
|
|
265
|
-
|
|
266
|
-
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Table_2.5_-
|
|
267
|
-
def _parse_packet_identifier(self):
|
|
268
|
-
offset = len(self._packet) - self.remaining_length
|
|
269
|
-
self.packet_identifier = self._packet[offset : offset + 2]
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def send_to_websocket(command, message):
|
|
275
|
-
if ws.connected:
|
|
276
|
-
data = {
|
|
277
|
-
"command": command,
|
|
278
|
-
"message": message
|
|
279
|
-
}
|
|
280
|
-
ws.send(json.dumps(data))
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
def tcp_message(flow: tcp.TCPFlow):
|
|
284
|
-
message = flow.messages[-1]
|
|
285
|
-
|
|
286
|
-
mqtt_packet = MQTTControlPacket(message.content)
|
|
287
|
-
|
|
288
|
-
log_message = mqtt_packet.pprint()
|
|
289
|
-
ctx.log.info(log_message)
|
|
290
|
-
|
|
291
|
-
if log_message.startswith("[PUBLISH]") and len(log_message) > len("[PUBLISH] "):
|
|
292
|
-
send_to_websocket("sniffing_decrypt", log_message)
|
|
293
|
-
|
|
294
|
-
# This way we can save topics
|
|
295
|
-
# if mqtt_packet.packet_type == mqtt_packet.PUBLISH:
|
|
296
|
-
# with open("topics.txt", "a") as f:
|
|
297
|
-
# f.write(f"{mqtt_packet.topic_name}\n")
|
|
298
|
-
# elif mqtt_packet.packet_type == mqtt_packet.SUBSCRIBE:
|
|
299
|
-
# with open("topics.txt", "a") as f:
|
|
300
|
-
# f.write(f"{mqtt_packet.topic_filters}\n")
|
package/roborockLib/test.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
const Roborock = require('./roborockAPI.js').Roborock;
|
|
2
|
-
|
|
3
|
-
var roborock = new Roborock({username: "tasict@gmail.com", debug: true});
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
roborock.setDeviceNotify(function(id, homeData){
|
|
7
|
-
console.info(`${id} deviceNotify:${JSON.stringify(homeData)}`);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
roborock.startService(function(){
|
|
11
|
-
|
|
12
|
-
if(roborock.isInited()){
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
roborock.getVacuumList().forEach(function(device){
|
|
16
|
-
var duid = device.duid;
|
|
17
|
-
var name = device.name;
|
|
18
|
-
var battery = roborock.getVacuumDeviceStatus(duid, "battery");
|
|
19
|
-
var model = roborock.getProductAttribute(duid, "model");
|
|
20
|
-
console.error("Name: " + name + " Battery: " + battery + " Model: " +model);
|
|
21
|
-
|
|
22
|
-
//roborock.startCommand(duid, "set_repeat", "2");
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
//polling status every 10 seconds
|
|
26
|
-
//setInterval(function(){
|
|
27
|
-
// roborock.getStatus(duid);
|
|
28
|
-
// console.info("polling status every 10 seconds");
|
|
29
|
-
//}, 10000);
|
|
30
|
-
|
|
31
|
-
//printing all process Scene
|
|
32
|
-
roborock.getScenes().then(function(scenes){
|
|
33
|
-
console.info("All Scenes:", JSON.stringify(scenes, null, 2));
|
|
34
|
-
|
|
35
|
-
// Test getScenesForDevice(duid) for this specific device
|
|
36
|
-
const deviceScenes = roborock.getScenesForDevice(duid);
|
|
37
|
-
console.info(`Scenes for device ${duid}:`, JSON.stringify(deviceScenes, null, 2));
|
|
38
|
-
}).catch(function(error){
|
|
39
|
-
console.error("Error getting scenes:", error.message);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
});
|