@apocaliss92/nodedreame 1.0.0
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/LICENSE +24 -0
- package/README.md +301 -0
- package/dist/index.cjs +3827 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1493 -0
- package/dist/index.d.ts +1493 -0
- package/dist/index.js +3760 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Tasshack (dreame-vacuum)
|
|
4
|
+
Copyright (c) 2025 Anton Daubert (dreame-mower)
|
|
5
|
+
Copyright (c) 2026 Martin Ellis (node-dreame)
|
|
6
|
+
Copyright (c) 2026 apocaliss92 (nodedreame Node.js port)
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
in the Software without restriction, including without limitation the rights
|
|
11
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
furnished to do so, subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# nodedreame
|
|
2
|
+
|
|
3
|
+
Node.js/TypeScript client for Dreame robot vacuums and mowers via the Dreamehome cloud.
|
|
4
|
+
|
|
5
|
+
> Work in progress. Unified, event-driven library — not a UI.
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
Phase 4 complete: on top of the Phase 2 generic handle, `discoverDevices()` now
|
|
10
|
+
returns a typed `VacuumDevice` for `dreame.vacuum.*` models and a typed
|
|
11
|
+
`MowerDevice` for `dreame.mower.*` models, each with decoded state getters and
|
|
12
|
+
capability-gated commands. Live map / pose-coverage track decoding (Phase 5) is
|
|
13
|
+
not implemented yet.
|
|
14
|
+
|
|
15
|
+
Under the hood:
|
|
16
|
+
|
|
17
|
+
- OAuth password-grant login with proactive token refresh (the shared session
|
|
18
|
+
is refreshed ~100s before expiry, and every device's MQTT push is
|
|
19
|
+
re-authenticated with the new token transparently).
|
|
20
|
+
- Per-device MQTT push with **durable reconnect** — it reconnects with backoff
|
|
21
|
+
on an unexpected drop and rebuilds the connection with a fresh token on
|
|
22
|
+
refresh. A `get_properties` poll fallback runs only while the push is down and
|
|
23
|
+
stops once it reconnects.
|
|
24
|
+
- All cloud and MQTT responses are validated with `zod` at the boundary, so a
|
|
25
|
+
malformed response fails fast with a clear error instead of propagating
|
|
26
|
+
`undefined`.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { Nodreame } from '@apocaliss92/nodedreame';
|
|
32
|
+
|
|
33
|
+
const client = new Nodreame({
|
|
34
|
+
username: process.env.DREAME_USERNAME!,
|
|
35
|
+
password: process.env.DREAME_PASSWORD!,
|
|
36
|
+
region: 'eu',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await client.login();
|
|
40
|
+
const devices = await client.discoverDevices();
|
|
41
|
+
|
|
42
|
+
for (const device of devices) {
|
|
43
|
+
console.log(device.deviceId, device.model, device.name);
|
|
44
|
+
|
|
45
|
+
// Live-read a couple of MIoT properties (siid.piid are model-specific).
|
|
46
|
+
await device.refreshProperties([{ siid: 2, piid: 1 }]);
|
|
47
|
+
console.log('state:', device.getProperty(2, 1)?.value);
|
|
48
|
+
|
|
49
|
+
// React to pushed updates.
|
|
50
|
+
device.on('stateChanged', (e) => console.log('changed', e.changes));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The shared session auto-refreshes ~100s before expiry; every device's MQTT
|
|
54
|
+
// push is re-authenticated with the new token transparently.
|
|
55
|
+
|
|
56
|
+
await client.close(); // closes all pushes, clears all timers
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
> The generic MIoT primitives stay available on every handle:
|
|
60
|
+
> `refreshProperties` / `getProperty` (cache) / `setProperty` / `callAction`.
|
|
61
|
+
> Mower (`startMowing`/schedules) handles and live maps arrive in later phases.
|
|
62
|
+
|
|
63
|
+
## Vacuums
|
|
64
|
+
|
|
65
|
+
For `dreame.vacuum.*` models, `discoverDevices()` returns a `VacuumDevice` (a
|
|
66
|
+
subclass of the generic handle), which adds typed, decoded state getters and
|
|
67
|
+
capability-gated commands on top of the raw MIoT primitives.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { Nodreame, VacuumDevice, SuctionLevel } from '@apocaliss92/nodedreame';
|
|
71
|
+
|
|
72
|
+
const client = new Nodreame({
|
|
73
|
+
username: process.env.DREAME_USERNAME!,
|
|
74
|
+
password: process.env.DREAME_PASSWORD!,
|
|
75
|
+
region: 'eu',
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await client.login();
|
|
79
|
+
const devices = await client.discoverDevices();
|
|
80
|
+
|
|
81
|
+
const vacuum = devices.find((d): d is VacuumDevice => d instanceof VacuumDevice);
|
|
82
|
+
if (vacuum) {
|
|
83
|
+
// Seed the cache with the vacuum's known properties (one live read).
|
|
84
|
+
await vacuum.refreshProperties([...VacuumDevice.DEFAULT_PROPS]);
|
|
85
|
+
|
|
86
|
+
// Typed, decoded state. Each getter returns null until the matching
|
|
87
|
+
// property has landed (via the seed read above or a pushed update).
|
|
88
|
+
console.log('status:', vacuum.status); // MiotState | null
|
|
89
|
+
console.log('battery:', vacuum.battery); // number | null (%)
|
|
90
|
+
console.log('suction:', vacuum.suction); // SuctionLevel | null
|
|
91
|
+
console.log('water:', vacuum.water); // WaterVolume | null
|
|
92
|
+
console.log('docked:', vacuum.isDocked);
|
|
93
|
+
console.log('faults:', vacuum.faults); // number[]
|
|
94
|
+
|
|
95
|
+
// Capability-gated commands (all async).
|
|
96
|
+
await vacuum.setSuction(SuctionLevel.Max);
|
|
97
|
+
await vacuum.startCleaning(); // begins a clean (NOT the lifecycle start())
|
|
98
|
+
// await vacuum.pause();
|
|
99
|
+
// await vacuum.stop();
|
|
100
|
+
// await vacuum.dock(); // return to dock / charge
|
|
101
|
+
// await vacuum.locate(); // make the robot beep
|
|
102
|
+
// await vacuum.cleanSegments([1, 2]); // per-room, gated by canCleanPerRoom
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await client.close();
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
> **`startCleaning()`, not `start()`.** `start()` is the inherited lifecycle
|
|
109
|
+
> method that opens the MQTT push; the cleaning command is `startCleaning()`.
|
|
110
|
+
> All command methods are `async`.
|
|
111
|
+
|
|
112
|
+
### Honest caveats
|
|
113
|
+
|
|
114
|
+
- **State getters return `null` until data lands.** Call
|
|
115
|
+
`refreshProperties([...VacuumDevice.DEFAULT_PROPS])` (or wait for a pushed
|
|
116
|
+
update) before reading; an unseeded getter is `null`, and an out-of-range raw
|
|
117
|
+
value also decodes to `null` (the raw integer is still available via the
|
|
118
|
+
`*Raw` getters, e.g. `vacuum.suctionRaw`).
|
|
119
|
+
- **`r2538z` capabilities are assumed, not verified.** The user's
|
|
120
|
+
`dreame.vacuum.r2538z` capability record is mirrored from its `r2532a` (X50)
|
|
121
|
+
sibling, so `vacuum.vacuumCapabilities.verified === false`. Treat its feature
|
|
122
|
+
flags as a best-effort hypothesis until confirmed on-device.
|
|
123
|
+
- **Clean-mode writes are safe by construction.** `setCleaningMode()` writes the
|
|
124
|
+
plain `CLEAN_MODE_SETTING` property (siid 2 piid 6); the raw `0x1400`-masked
|
|
125
|
+
bitfield (siid 4 piid 23) is read-only here and never written directly.
|
|
126
|
+
- **Some action mappings are assumed.** `pause`/`stop`/`locate`/`clearWarning`
|
|
127
|
+
are wire-verified on the r2532a sibling; `startCleaning`/`dock` and the
|
|
128
|
+
targeted-clean payloads (`cleanSegments`/`cleanZones`/`cleanSpot`) are ported
|
|
129
|
+
from Tasshack and not yet live-verified across all models.
|
|
130
|
+
- **No map-derived state yet.** Per-room/current-segment data and live maps come
|
|
131
|
+
from the map layer (Phase 5); they are not exposed in Phase 3.
|
|
132
|
+
|
|
133
|
+
## Mowers
|
|
134
|
+
|
|
135
|
+
For `dreame.mower.*` models, `discoverDevices()` returns a `MowerDevice` (a
|
|
136
|
+
subclass of the generic handle) with typed, decoded state getters and
|
|
137
|
+
capability-gated commands, mirroring the vacuum surface.
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { Nodreame, MowerDevice } from '@apocaliss92/nodedreame';
|
|
141
|
+
|
|
142
|
+
const client = new Nodreame({
|
|
143
|
+
username: process.env.DREAME_USERNAME!,
|
|
144
|
+
password: process.env.DREAME_PASSWORD!,
|
|
145
|
+
region: 'eu',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await client.login();
|
|
149
|
+
const devices = await client.discoverDevices();
|
|
150
|
+
|
|
151
|
+
const mower = devices.find((d): d is MowerDevice => d instanceof MowerDevice);
|
|
152
|
+
if (mower) {
|
|
153
|
+
// Seed the cache with the mower's known properties (one live read).
|
|
154
|
+
await mower.refreshProperties([...MowerDevice.DEFAULT_PROPS]);
|
|
155
|
+
|
|
156
|
+
// Typed, decoded state. Each getter returns null until the matching
|
|
157
|
+
// property has landed (via the seed read above or a pushed update).
|
|
158
|
+
console.log('status:', mower.status); // MowerStatus | null
|
|
159
|
+
console.log('battery:', mower.battery); // number | null (%)
|
|
160
|
+
console.log('charging:', mower.charging); // MowerChargingStatus | null
|
|
161
|
+
console.log('docked:', mower.isDocked); // boolean
|
|
162
|
+
console.log('mowing:', mower.isMowing); // boolean
|
|
163
|
+
console.log('task:', mower.task); // MowerTaskDescriptor | null (2:50)
|
|
164
|
+
console.log('coverage target %:', mower.coverageTargetPct); // number | null
|
|
165
|
+
console.log('control action:', mower.controlAction); // MowerControlAction | null
|
|
166
|
+
|
|
167
|
+
// Capability-gated commands (all async).
|
|
168
|
+
await mower.startMowing(); // begins mowing (NOT the lifecycle start())
|
|
169
|
+
// await mower.pause();
|
|
170
|
+
// await mower.stop();
|
|
171
|
+
// await mower.dock(); // return to dock / charge
|
|
172
|
+
// await mower.resume(); // resume after a pause (continueControl opcode)
|
|
173
|
+
// Targeted starts (gated by the model's capability flags):
|
|
174
|
+
// await mower.startMowingAllArea(mapId); // whole map
|
|
175
|
+
// await mower.startMowingZones([1, 3]); // selected zones
|
|
176
|
+
// await mower.startMowingEdges([[1, 0]]); // edge / contour pairs
|
|
177
|
+
// await mower.startMowingSpots([5]); // spot areas
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
await client.close();
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
> **`startMowing()`, not `start()`.** `start()` is the inherited lifecycle
|
|
184
|
+
> method that opens the MQTT push; the mowing command is `startMowing()`. All
|
|
185
|
+
> command methods are `async`.
|
|
186
|
+
|
|
187
|
+
### Honest caveats
|
|
188
|
+
|
|
189
|
+
- **State getters return `null` until data lands.** Call
|
|
190
|
+
`refreshProperties([...MowerDevice.DEFAULT_PROPS])` (or wait for a pushed
|
|
191
|
+
update) before reading; an unseeded getter is `null`, and an out-of-range raw
|
|
192
|
+
value also decodes to `null` (the raw integer is still available via the
|
|
193
|
+
`*Raw` getters, e.g. `mower.statusRaw`, `mower.chargingRaw`,
|
|
194
|
+
`mower.taskStatusRaw`).
|
|
195
|
+
- **`dreame.mower.p2255` (Dreame A1) capabilities are assumed, not verified.**
|
|
196
|
+
The donor integration has no per-model mower capability matrix, so the
|
|
197
|
+
targeted-mowing flags are a conservative hypothesis from its command surface;
|
|
198
|
+
`mower.mowerCapabilities.verified === false` until confirmed on-device.
|
|
199
|
+
Unsupported targeted starts throw `DreameError`.
|
|
200
|
+
- **Progress is a coverage scalar, not a map track.** `coverageTargetPct`
|
|
201
|
+
surfaces the scheduling-task descriptor's coverage target (`d.o`, 2:50); the
|
|
202
|
+
byte-accurate pose/coverage track geometry and live maps come from the map
|
|
203
|
+
layer (Phase 5) and are not decoded here.
|
|
204
|
+
|
|
205
|
+
The error classes and core domain types are also exported so consumers can catch
|
|
206
|
+
and type cloud failures:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import {
|
|
210
|
+
DreameError,
|
|
211
|
+
DreameAuthError,
|
|
212
|
+
DreameApiError,
|
|
213
|
+
DreameDeviceOfflineError,
|
|
214
|
+
DreameTransportError,
|
|
215
|
+
} from '@apocaliss92/nodedreame';
|
|
216
|
+
import type { DreameSession, DreameDevice, MiotProp } from '@apocaliss92/nodedreame';
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Maps
|
|
220
|
+
|
|
221
|
+
Both device families decode their on-device map and render it to an image. The
|
|
222
|
+
binary/JSON decoders, the OSS signed-blob fetcher and every intermediate step
|
|
223
|
+
stay private — you obtain maps through the device handles and (optionally) the
|
|
224
|
+
two renderers.
|
|
225
|
+
|
|
226
|
+
### Vacuum map → PNG
|
|
227
|
+
|
|
228
|
+
`VacuumDevice.getMap()` resolves a saved/live map blob, decrypts and inflates
|
|
229
|
+
the binary envelope, parses the 27-byte header and the `fsm:1` pixel grid, and
|
|
230
|
+
returns a structured `VacuumMap` — segments/rooms, the cleaning path, AI
|
|
231
|
+
obstacles, virtual walls, no-go / no-mop zones, sneak zones, per-room walls and
|
|
232
|
+
the cleaned-area overlay. `renderVacuumPng(map)` rasterises it to a PNG
|
|
233
|
+
`Buffer` via `pngjs`.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
import { renderVacuumPng } from '@apocaliss92/nodedreame';
|
|
237
|
+
import type { VacuumMap } from '@apocaliss92/nodedreame';
|
|
238
|
+
import { writeFile } from 'node:fs/promises';
|
|
239
|
+
|
|
240
|
+
// `filename` is the OSS object name advertised on the map PATH push (siid 6,
|
|
241
|
+
// piid 3); resolve it from a `mapInfo` push before calling getMap().
|
|
242
|
+
const map: VacuumMap = await vacuum.getMap({ filename });
|
|
243
|
+
const png = renderVacuumPng(map); // Buffer (optional { scale } upscale)
|
|
244
|
+
await writeFile('map.png', png);
|
|
245
|
+
|
|
246
|
+
// The most-recently-decoded map is cached, and the active room id is derived:
|
|
247
|
+
vacuum.lastMap; // VacuumMap | null
|
|
248
|
+
vacuum.currentSegmentId; // number | null (id of the active segment)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
> **Live map data requires an awake robot.** A sleeping vacuum returns no fresh
|
|
252
|
+
> blob. `getMap()` decodes a single frame; continuous live-frame **P-frame
|
|
253
|
+
> streaming** (merging delta frames as the robot moves) is a documented
|
|
254
|
+
> follow-up — the `applyVacuumPFrame` merge primitive ships and is unit-tested,
|
|
255
|
+
> so that work is additive, not a rewrite.
|
|
256
|
+
|
|
257
|
+
### Mower map → SVG
|
|
258
|
+
|
|
259
|
+
`MowerDevice.getMap()` reassembles the batched `MAP.*` / `M_PATH.*` JSON chunks
|
|
260
|
+
and parses them into a `MowerMap` — zones, spot areas, forbidden areas,
|
|
261
|
+
navigation paths, contours, mow-path tracks and the map boundary.
|
|
262
|
+
`mower.mapSvg()` (or the free `renderMowerSvg(map)`) renders a deterministic SVG
|
|
263
|
+
string of the geometry.
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
import { renderMowerSvg } from '@apocaliss92/nodedreame';
|
|
267
|
+
import type { MowerMap } from '@apocaliss92/nodedreame';
|
|
268
|
+
import { writeFile } from 'node:fs/promises';
|
|
269
|
+
|
|
270
|
+
const map: MowerMap = await mower.getMap();
|
|
271
|
+
const svg = await mower.mapSvg(); // or renderMowerSvg(map)
|
|
272
|
+
await writeFile('map.svg', svg);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
> **Same awake-robot caveat.** A sleeping mower returns no fresh batch.
|
|
276
|
+
> Additionally, the concrete **live batch-fetch cloud endpoint is a documented
|
|
277
|
+
> follow-up**: its path is obfuscated in the donor integration and not yet
|
|
278
|
+
> recovered, so the default fetcher throws (`getMap()` then rejects). The map
|
|
279
|
+
> **parser and SVG renderer are fully shipped and unit-tested** against batch
|
|
280
|
+
> fixtures, and `MowerDevice` accepts an injected batch fetcher seam, so a
|
|
281
|
+
> caller that already knows the path can drive a live map today.
|
|
282
|
+
|
|
283
|
+
### Attribution
|
|
284
|
+
|
|
285
|
+
The vacuum map decoder is ported from
|
|
286
|
+
[malard/node-dreame](https://github.com/malard/node-dreame); the mower map
|
|
287
|
+
parser and SVG renderer from
|
|
288
|
+
[antondaubert/dreame-mower](https://github.com/antondaubert/dreame-mower). Both
|
|
289
|
+
are MIT — see `LICENSE`.
|
|
290
|
+
|
|
291
|
+
## Install
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
npm install @apocaliss92/nodedreame
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## License
|
|
298
|
+
|
|
299
|
+
MIT. Ports prior work from [Tasshack/dreame-vacuum](https://github.com/Tasshack/dreame-vacuum),
|
|
300
|
+
[antondaubert/dreame-mower](https://github.com/antondaubert/dreame-mower), and
|
|
301
|
+
[malard/node-dreame](https://github.com/malard/node-dreame); see `LICENSE`.
|