@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/dist/index.d.cts
ADDED
|
@@ -0,0 +1,1493 @@
|
|
|
1
|
+
import { fetch } from 'undici';
|
|
2
|
+
|
|
3
|
+
/** Public name of this library. */
|
|
4
|
+
declare const LIBRARY_NAME = "nodedreame";
|
|
5
|
+
|
|
6
|
+
declare class DreameError extends Error {
|
|
7
|
+
readonly cause?: unknown | undefined;
|
|
8
|
+
constructor(message: string, cause?: unknown | undefined);
|
|
9
|
+
}
|
|
10
|
+
declare class DreameAuthError extends DreameError {
|
|
11
|
+
readonly status?: number | undefined;
|
|
12
|
+
constructor(message: string, status?: number | undefined, cause?: unknown);
|
|
13
|
+
}
|
|
14
|
+
declare class DreameApiError extends DreameError {
|
|
15
|
+
readonly status: number;
|
|
16
|
+
readonly body?: unknown | undefined;
|
|
17
|
+
constructor(message: string, status: number, body?: unknown | undefined, cause?: unknown);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Thrown when the cloud returns code 80001. The literal `msg` claims the
|
|
21
|
+
* device is offline — that interpretation is FREQUENTLY WRONG. 80001 means
|
|
22
|
+
* the cloud's HTTP-side waiter for the device's MQTT ACK gave up after ~8s.
|
|
23
|
+
* It does NOT prove the action failed to reach the device. The MQTT
|
|
24
|
+
* subscription is the source of truth for reachability. The only true
|
|
25
|
+
* positive is a genuinely unreachable device (powered off / network lost /
|
|
26
|
+
* rebooting) — in which case no MQTT echo arrives either.
|
|
27
|
+
*/
|
|
28
|
+
declare class DreameDeviceOfflineError extends DreameApiError {
|
|
29
|
+
constructor(message: string, status: number, body?: unknown);
|
|
30
|
+
}
|
|
31
|
+
declare class DreameTransportError extends DreameError {
|
|
32
|
+
constructor(message: string, cause?: unknown);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Supported Dreame cloud regions. */
|
|
36
|
+
type DreameRegion = 'eu' | 'us' | 'cn' | 'ru' | 'sg' | 'in' | 'de' | 'tw';
|
|
37
|
+
|
|
38
|
+
interface DreameSession {
|
|
39
|
+
accessToken: string;
|
|
40
|
+
refreshToken?: string | undefined;
|
|
41
|
+
uid: string;
|
|
42
|
+
/** Epoch-ms at which `accessToken` expires (`Date.now() + expires_in*1000`). */
|
|
43
|
+
expiresAt: number;
|
|
44
|
+
region: DreameRegion;
|
|
45
|
+
}
|
|
46
|
+
interface DreameDevice {
|
|
47
|
+
did: string;
|
|
48
|
+
model: string;
|
|
49
|
+
name: string;
|
|
50
|
+
mac?: string | undefined;
|
|
51
|
+
online: boolean;
|
|
52
|
+
/** Raw record from the cloud, kept for forward compatibility (incl. bindDomain). */
|
|
53
|
+
raw: Record<string, unknown>;
|
|
54
|
+
firmwareVersion?: string;
|
|
55
|
+
serialNumber?: string;
|
|
56
|
+
cloudState?: DreameCloudState;
|
|
57
|
+
}
|
|
58
|
+
/** Cloud-cached subset of device state distilled from the device-list response. */
|
|
59
|
+
interface DreameCloudState {
|
|
60
|
+
/** Most-recent MIoT state int (siid 2 piid 1). */
|
|
61
|
+
latestStatus: number | null;
|
|
62
|
+
/** Battery percentage 0-100. */
|
|
63
|
+
battery: number | null;
|
|
64
|
+
/** Camera/LinkVisual session active? Derived from the `videoStatus` JSON string. */
|
|
65
|
+
videoActive: boolean | null;
|
|
66
|
+
/** `featureCode2` capability bitfield. */
|
|
67
|
+
featureCode2: number | null;
|
|
68
|
+
}
|
|
69
|
+
/** A single MIoT property reference (service + property id). */
|
|
70
|
+
interface MiotProp {
|
|
71
|
+
siid: number;
|
|
72
|
+
piid: number;
|
|
73
|
+
}
|
|
74
|
+
/** A single MIoT action reference (service + action id) with optional inputs. */
|
|
75
|
+
interface MiotAction {
|
|
76
|
+
siid: number;
|
|
77
|
+
aiid: number;
|
|
78
|
+
in?: unknown[];
|
|
79
|
+
}
|
|
80
|
+
/** Property write — `MiotProp` plus the value to set. */
|
|
81
|
+
interface PropertyWrite extends MiotProp {
|
|
82
|
+
value: unknown;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Per-property result returned by the cloud. All fields are optional: the cloud
|
|
86
|
+
* response shape is not fully observed (the command path is not exercised by the
|
|
87
|
+
* live e2e), so we validate structure leniently rather than risk rejecting real
|
|
88
|
+
* data. Mirrors `PropertyResultSchema` in transport/schemas.ts.
|
|
89
|
+
*/
|
|
90
|
+
interface PropertyResult {
|
|
91
|
+
siid?: number | undefined;
|
|
92
|
+
piid?: number | undefined;
|
|
93
|
+
value?: unknown;
|
|
94
|
+
code?: number | undefined;
|
|
95
|
+
[key: string]: unknown;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Narrow fetch-shaped function type backed by undici. Defined here so all
|
|
100
|
+
* callers share one declaration — no DOM lib globals, no casts.
|
|
101
|
+
*
|
|
102
|
+
* We re-export `FetchImpl` from this module as the canonical definition;
|
|
103
|
+
* `transport/http.ts` re-exports it from here so downstream modules have a
|
|
104
|
+
* single import path.
|
|
105
|
+
*/
|
|
106
|
+
type FetchImpl = typeof fetch;
|
|
107
|
+
|
|
108
|
+
interface RequestContextOpts {
|
|
109
|
+
region: DreameRegion;
|
|
110
|
+
country?: string;
|
|
111
|
+
lang?: string;
|
|
112
|
+
host?: string;
|
|
113
|
+
fetchImpl?: FetchImpl;
|
|
114
|
+
}
|
|
115
|
+
interface RequestContextInput {
|
|
116
|
+
region: DreameRegion;
|
|
117
|
+
country?: string | undefined;
|
|
118
|
+
lang?: string | undefined;
|
|
119
|
+
/** Override host. Some callers spell this `authHost`/`apiHost` — pass that here. */
|
|
120
|
+
host?: string | undefined;
|
|
121
|
+
fetchImpl?: FetchImpl | undefined;
|
|
122
|
+
}
|
|
123
|
+
declare class RequestContext {
|
|
124
|
+
readonly region: DreameRegion;
|
|
125
|
+
readonly country: string;
|
|
126
|
+
readonly lang: string;
|
|
127
|
+
readonly host: string;
|
|
128
|
+
readonly fetchImpl: FetchImpl;
|
|
129
|
+
constructor(opts: RequestContextOpts);
|
|
130
|
+
static from(input: RequestContextInput): RequestContext;
|
|
131
|
+
/** `https://<host><path>` — pass a path with a leading slash. */
|
|
132
|
+
url(path: string): string;
|
|
133
|
+
/** Build the static Dreame headers, optionally with a bearer token + content-type. */
|
|
134
|
+
headers(opts?: {
|
|
135
|
+
accessToken?: string | null;
|
|
136
|
+
contentType?: string;
|
|
137
|
+
}): Record<string, string>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface CommonInput {
|
|
141
|
+
session: DreameSession;
|
|
142
|
+
region: DreameRegion;
|
|
143
|
+
did: string;
|
|
144
|
+
ctx?: RequestContext;
|
|
145
|
+
country?: string;
|
|
146
|
+
lang?: string;
|
|
147
|
+
apiHost?: string;
|
|
148
|
+
/**
|
|
149
|
+
* Inject a fetch implementation. Placed here (on `CommonInput`) so tests can
|
|
150
|
+
* pass a mock via the first argument without any casts. Do NOT put this on
|
|
151
|
+
* `CallOptions` — it belongs on the base/donor argument only.
|
|
152
|
+
*/
|
|
153
|
+
fetchImpl?: FetchImpl;
|
|
154
|
+
signal?: AbortSignal;
|
|
155
|
+
timeoutMs?: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A strongly-typed wrapper over Node's EventEmitter. */
|
|
159
|
+
declare class TypedEmitter<Events extends Record<string, unknown[]>> {
|
|
160
|
+
#private;
|
|
161
|
+
on<K extends keyof Events & string>(event: K, listener: (...args: Events[K]) => void): this;
|
|
162
|
+
once<K extends keyof Events & string>(event: K, listener: (...args: Events[K]) => void): this;
|
|
163
|
+
off<K extends keyof Events & string>(event: K, listener: (...args: Events[K]) => void): this;
|
|
164
|
+
emit<K extends keyof Events & string>(event: K, ...args: Events[K]): boolean;
|
|
165
|
+
removeAllListeners(): this;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** A single property update pushed by the device. */
|
|
169
|
+
interface PropertyChange {
|
|
170
|
+
did: string;
|
|
171
|
+
siid: number;
|
|
172
|
+
piid: number;
|
|
173
|
+
value: unknown;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* A read-only set of capability tokens a device model supports. Phase 2 ships
|
|
178
|
+
* only the shape + a no-op default; P3/P4 populate per-model tables behind the
|
|
179
|
+
* same {@link CapabilityResolver} interface.
|
|
180
|
+
*/
|
|
181
|
+
interface DeviceCapabilities {
|
|
182
|
+
readonly model: string;
|
|
183
|
+
/** Whether the model is known to support a capability token. */
|
|
184
|
+
has(token: string): boolean;
|
|
185
|
+
/** All known capability tokens for the model. */
|
|
186
|
+
list(): readonly string[];
|
|
187
|
+
}
|
|
188
|
+
/** Resolves the capability set for a device model string. */
|
|
189
|
+
interface CapabilityResolver {
|
|
190
|
+
resolve(model: string): DeviceCapabilities;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Default resolver: every model resolves to an EMPTY capability set. This is a
|
|
194
|
+
* deliberate no-op scaffold — device-family resolvers (vacuum/mower) override
|
|
195
|
+
* `resolve` in later phases while keeping this interface stable.
|
|
196
|
+
*/
|
|
197
|
+
declare class DefaultCapabilityResolver implements CapabilityResolver {
|
|
198
|
+
resolve(model: string): DeviceCapabilities;
|
|
199
|
+
}
|
|
200
|
+
/** Convenience: resolve capabilities via the shared default resolver. */
|
|
201
|
+
declare function resolveCapabilities(model: string): DeviceCapabilities;
|
|
202
|
+
|
|
203
|
+
/** Options for constructing a {@link Nodreame} facade. */
|
|
204
|
+
interface NodreameOptions {
|
|
205
|
+
/** Account email / username for the Dreamehome cloud. */
|
|
206
|
+
username: string;
|
|
207
|
+
/** Account password (hashed client-side before transmission). */
|
|
208
|
+
password: string;
|
|
209
|
+
/** Dreame cloud region. */
|
|
210
|
+
region: DreameRegion;
|
|
211
|
+
/** ISO-3166 alpha-2 country override (defaults from region). */
|
|
212
|
+
country?: string;
|
|
213
|
+
/** ISO-639-1 language override (defaults from region). */
|
|
214
|
+
lang?: string;
|
|
215
|
+
/**
|
|
216
|
+
* Brand tenant. Reserved for Mova devices; Phase 2 only wires Dreame, so this
|
|
217
|
+
* is accepted but not yet branched on. Defaults to `'dreame'`.
|
|
218
|
+
*/
|
|
219
|
+
accountType?: 'dreame' | 'mova';
|
|
220
|
+
/**
|
|
221
|
+
* Seconds before access-token expiry at which the session is proactively
|
|
222
|
+
* refreshed. Defaults to 100 (parity with the donor client).
|
|
223
|
+
*/
|
|
224
|
+
refreshLeewaySecs?: number;
|
|
225
|
+
/**
|
|
226
|
+
* Poll interval (ms) used as a fallback when a device's MQTT push is down.
|
|
227
|
+
* Defaults to 30000. Set to 0 to disable poll fallback entirely.
|
|
228
|
+
*/
|
|
229
|
+
pollIntervalMs?: number;
|
|
230
|
+
/**
|
|
231
|
+
* Whether each device should eagerly seed its property cache on construction
|
|
232
|
+
* via a `get_properties` read. Defaults to `true` (opt-out).
|
|
233
|
+
*/
|
|
234
|
+
fetchInitialValues?: boolean;
|
|
235
|
+
/** Inject a fetch implementation (testing/advanced). */
|
|
236
|
+
fetchImpl?: FetchImpl;
|
|
237
|
+
}
|
|
238
|
+
/** One cached property value plus when it was last observed. */
|
|
239
|
+
interface PropertyState {
|
|
240
|
+
siid: number;
|
|
241
|
+
piid: number;
|
|
242
|
+
value: unknown;
|
|
243
|
+
/** Epoch-ms of the last update (push or live read). */
|
|
244
|
+
updatedAt: number;
|
|
245
|
+
}
|
|
246
|
+
/** Emitted when a single property changes (cache delta). */
|
|
247
|
+
interface PropertyChangedEvent {
|
|
248
|
+
deviceId: string;
|
|
249
|
+
siid: number;
|
|
250
|
+
piid: number;
|
|
251
|
+
value: unknown;
|
|
252
|
+
/** Prior cached value, or `null` if the property was previously unknown. */
|
|
253
|
+
previousValue: unknown;
|
|
254
|
+
}
|
|
255
|
+
/** Emitted once per push batch, aggregating all property changes in it. */
|
|
256
|
+
interface StateChangedEvent {
|
|
257
|
+
deviceId: string;
|
|
258
|
+
changes: PropertyChangedEvent[];
|
|
259
|
+
}
|
|
260
|
+
/** Emitted on a device MIoT event (`event_occured`). */
|
|
261
|
+
interface DeviceEvent {
|
|
262
|
+
deviceId: string;
|
|
263
|
+
siid: number;
|
|
264
|
+
eiid: number;
|
|
265
|
+
arguments: unknown[];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The slice of {@link DreamePush} that {@link BaseDevice} depends on. Declaring
|
|
270
|
+
* it as an interface lets tests inject a fake emitter with no casts; the real
|
|
271
|
+
* `DreamePush` structurally satisfies it.
|
|
272
|
+
*/
|
|
273
|
+
interface PushLike {
|
|
274
|
+
on(event: 'properties', cb: (changes: PropertyChange[]) => void): this;
|
|
275
|
+
on(event: 'event', cb: (ev: {
|
|
276
|
+
did: string;
|
|
277
|
+
siid: number;
|
|
278
|
+
eiid: number;
|
|
279
|
+
arguments: unknown[];
|
|
280
|
+
}) => void): this;
|
|
281
|
+
on(event: 'connect', cb: () => void): this;
|
|
282
|
+
on(event: 'close', cb: () => void): this;
|
|
283
|
+
on(event: 'error', cb: (err: Error) => void): this;
|
|
284
|
+
open(): Promise<void>;
|
|
285
|
+
close(): Promise<void>;
|
|
286
|
+
refreshSession(session: DreameSession): Promise<void>;
|
|
287
|
+
}
|
|
288
|
+
/** Injectable collaborators — defaults wire the real transport + command layer. */
|
|
289
|
+
interface BaseDeviceDeps {
|
|
290
|
+
createPush(device: DreameDevice, session: DreameSession, region: DreameRegion): PushLike;
|
|
291
|
+
getProperties(base: CommonInput, props: MiotProp[]): Promise<PropertyResult[]>;
|
|
292
|
+
setProperties(base: CommonInput, writes: PropertyWrite[]): Promise<PropertyResult[]>;
|
|
293
|
+
callAction(base: CommonInput, action: {
|
|
294
|
+
siid: number;
|
|
295
|
+
aiid: number;
|
|
296
|
+
in?: unknown[];
|
|
297
|
+
}): Promise<unknown>;
|
|
298
|
+
}
|
|
299
|
+
type BaseDeviceEvents = {
|
|
300
|
+
propertyChanged: [PropertyChangedEvent];
|
|
301
|
+
stateChanged: [StateChangedEvent];
|
|
302
|
+
event: [DeviceEvent];
|
|
303
|
+
error: [Error];
|
|
304
|
+
};
|
|
305
|
+
interface BaseDeviceInput {
|
|
306
|
+
device: DreameDevice;
|
|
307
|
+
region: DreameRegion;
|
|
308
|
+
/** Always reads the LATEST session — the facade owns the variable. */
|
|
309
|
+
sessionRef: () => DreameSession;
|
|
310
|
+
deps?: BaseDeviceDeps;
|
|
311
|
+
/** Eager-seed the cache on `start()`. Default true. */
|
|
312
|
+
fetchInitialValues?: boolean;
|
|
313
|
+
/** Properties to seed when `fetchInitialValues` is true. Default `[]`. */
|
|
314
|
+
initialProps?: MiotProp[];
|
|
315
|
+
/** Poll interval (ms) while MQTT is down. Default 30000; 0 disables. */
|
|
316
|
+
pollIntervalMs?: number;
|
|
317
|
+
/** Capability set (defaults to the no-op resolver). */
|
|
318
|
+
capabilities?: DeviceCapabilities;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* A device-type-agnostic live handle for one Dreame device.
|
|
322
|
+
*
|
|
323
|
+
* Generic over its event map so subclasses can WIDEN the typed emitter with
|
|
324
|
+
* their own events (e.g. the vacuum's `'map'` event) without a banned cast.
|
|
325
|
+
* `Events` is constrained to extend {@link BaseDeviceEvents} so every internal
|
|
326
|
+
* `emit(...)` call stays type-safe, and it defaults to {@link BaseDeviceEvents}
|
|
327
|
+
* so a bare `BaseDevice` reference behaves exactly as before.
|
|
328
|
+
*/
|
|
329
|
+
declare class BaseDevice<Events extends BaseDeviceEvents = BaseDeviceEvents> extends TypedEmitter<Events> {
|
|
330
|
+
#private;
|
|
331
|
+
constructor(input: BaseDeviceInput);
|
|
332
|
+
get deviceId(): string;
|
|
333
|
+
get model(): string;
|
|
334
|
+
get name(): string;
|
|
335
|
+
get capabilities(): DeviceCapabilities;
|
|
336
|
+
/** Snapshot of all cached property states. */
|
|
337
|
+
get properties(): readonly PropertyState[];
|
|
338
|
+
/** Read a cached property, or `undefined` if never observed. */
|
|
339
|
+
getProperty(siid: number, piid: number): PropertyState | undefined;
|
|
340
|
+
/** The region this handle is bound to. Subclasses use it to build map fetches. */
|
|
341
|
+
protected get region(): DreameRegion;
|
|
342
|
+
/** Latest session snapshot. Subclasses use it for out-of-band fetches (maps). */
|
|
343
|
+
protected currentSession(): DreameSession;
|
|
344
|
+
/** Open the push, wire events, optionally seed the cache. */
|
|
345
|
+
start(): Promise<void>;
|
|
346
|
+
/** Live-read properties, update the cache, return the raw results. */
|
|
347
|
+
refreshProperties(props: MiotProp[]): Promise<PropertyResult[]>;
|
|
348
|
+
/** Write a property to the device. */
|
|
349
|
+
setProperty(write: PropertyWrite): Promise<PropertyResult[]>;
|
|
350
|
+
/** Invoke a MIoT action on the device. */
|
|
351
|
+
callAction(siid: number, aiid: number, input?: unknown[]): Promise<unknown>;
|
|
352
|
+
/** Propagate a refreshed session to the underlying push. */
|
|
353
|
+
applySession(session: DreameSession): Promise<void>;
|
|
354
|
+
/** Tear down: stop polling, close the push. */
|
|
355
|
+
close(): Promise<void>;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Args the facade passes to `createDevice` (a seam for tests). */
|
|
359
|
+
interface CreateDeviceArgs {
|
|
360
|
+
device: DreameDevice;
|
|
361
|
+
region: DreameRegion;
|
|
362
|
+
sessionRef: () => DreameSession;
|
|
363
|
+
}
|
|
364
|
+
/** Injectable collaborators — defaults wire the real P1 modules. */
|
|
365
|
+
interface NodreameDeps {
|
|
366
|
+
login(input: {
|
|
367
|
+
email: string;
|
|
368
|
+
password: string;
|
|
369
|
+
region: DreameRegion;
|
|
370
|
+
country?: string;
|
|
371
|
+
lang?: string;
|
|
372
|
+
fetchImpl?: FetchImpl;
|
|
373
|
+
}): Promise<DreameSession>;
|
|
374
|
+
refresh(input: {
|
|
375
|
+
refreshToken: string;
|
|
376
|
+
region: DreameRegion;
|
|
377
|
+
country?: string;
|
|
378
|
+
lang?: string;
|
|
379
|
+
fetchImpl?: FetchImpl;
|
|
380
|
+
}): Promise<DreameSession>;
|
|
381
|
+
listDevices(input: {
|
|
382
|
+
session: DreameSession;
|
|
383
|
+
region: DreameRegion;
|
|
384
|
+
fetchImpl?: FetchImpl;
|
|
385
|
+
}): Promise<DreameDevice[]>;
|
|
386
|
+
createDevice(args: CreateDeviceArgs): BaseDevice;
|
|
387
|
+
}
|
|
388
|
+
type NodreameEvents = {
|
|
389
|
+
/** Re-emitted device state change, tagged with the deviceId. */
|
|
390
|
+
stateChanged: [StateChangedEvent];
|
|
391
|
+
/** Re-emitted device event, tagged with the deviceId. */
|
|
392
|
+
event: [DeviceEvent];
|
|
393
|
+
error: [Error];
|
|
394
|
+
};
|
|
395
|
+
/** Public facade for the Dreamehome cloud. */
|
|
396
|
+
declare class Nodreame extends TypedEmitter<NodreameEvents> {
|
|
397
|
+
#private;
|
|
398
|
+
constructor(opts: NodreameOptions, deps?: NodreameDeps);
|
|
399
|
+
get region(): DreameRegion;
|
|
400
|
+
get session(): DreameSession | null;
|
|
401
|
+
get devices(): readonly BaseDevice[];
|
|
402
|
+
/** Authenticate and stash the single shared session. */
|
|
403
|
+
login(): Promise<DreameSession>;
|
|
404
|
+
/** Return a valid session, refreshing proactively within the leeway window. */
|
|
405
|
+
ensureSession(): Promise<DreameSession>;
|
|
406
|
+
/** Discover devices and build a live handle per device. */
|
|
407
|
+
discoverDevices(): Promise<readonly BaseDevice[]>;
|
|
408
|
+
/** Tear everything down: close every device push and clear timers. */
|
|
409
|
+
close(): Promise<void>;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Vacuum MIoT enum definitions (ported from malard/node-dreame
|
|
414
|
+
* src/spec/enums.ts, MIT — attribution retained). Only the state/command
|
|
415
|
+
* enums P3 needs are ported here; dock/schedule enums land in later phases.
|
|
416
|
+
* Annotations (VERIFIED <date> / ASSUMED from Tasshack) are preserved.
|
|
417
|
+
*/
|
|
418
|
+
/**
|
|
419
|
+
* VERIFIED on r2532a 2026-05-02 — MIoT STATE (siid 2 piid 1). Sourced from
|
|
420
|
+
* the device's keyDefine v8 JSON. 19 values verified live across a full
|
|
421
|
+
* cleaning task; sub-mode: value 1 = vacuum-only, value 12 = vacuum+mop.
|
|
422
|
+
*/
|
|
423
|
+
declare enum MiotState {
|
|
424
|
+
Cleaning = 1,
|
|
425
|
+
Standby = 2,
|
|
426
|
+
Paused = 3,
|
|
427
|
+
PausedAlt = 4,
|
|
428
|
+
ReturningToCharge = 5,
|
|
429
|
+
Charging = 6,
|
|
430
|
+
Mopping = 7,
|
|
431
|
+
MopDrying = 8,
|
|
432
|
+
MopCleaning = 9,
|
|
433
|
+
ReturningToWash = 10,
|
|
434
|
+
Mapping = 11,
|
|
435
|
+
CleaningAlt = 12,
|
|
436
|
+
ChargingComplete = 13,
|
|
437
|
+
Updating = 14,
|
|
438
|
+
CallToClean = 15,
|
|
439
|
+
AutoRepairBase = 16,
|
|
440
|
+
ReturnInstallMop = 17,
|
|
441
|
+
ReturnRemoveMop = 18,
|
|
442
|
+
WaterSupplyDrainTest = 19,
|
|
443
|
+
CleanMopRefillWater = 20,
|
|
444
|
+
PausedCleaning = 21,
|
|
445
|
+
AutoEmptying = 22,
|
|
446
|
+
RemoteCleaning = 23,
|
|
447
|
+
IntelligentCharging = 24,
|
|
448
|
+
SecondCleaning = 25,
|
|
449
|
+
Following = 26,
|
|
450
|
+
PartialCleaning = 27,
|
|
451
|
+
ReturnToEmpty = 28,
|
|
452
|
+
WaitingForTask = 29,
|
|
453
|
+
CleanWashboardBase = 30,
|
|
454
|
+
AutoWaterDraining = 33,
|
|
455
|
+
ShortcutRunning = 97,
|
|
456
|
+
CameraMonitoring = 98,
|
|
457
|
+
CameraMonitoringPaused = 99,
|
|
458
|
+
InitialDeepClean = 101
|
|
459
|
+
}
|
|
460
|
+
/** VERIFIED on r2532a 2026-05-02 — ChargingStatus (siid 3 piid 2). */
|
|
461
|
+
declare enum ChargingStatus {
|
|
462
|
+
Charging = 1,
|
|
463
|
+
Discharging = 2,
|
|
464
|
+
Returning = 5
|
|
465
|
+
}
|
|
466
|
+
/** VERIFIED on r2532a 2026-05-02 — SuctionLevel (siid 4 piid 4). X50 labels. */
|
|
467
|
+
declare enum SuctionLevel {
|
|
468
|
+
Quiet = 0,
|
|
469
|
+
Standard = 1,
|
|
470
|
+
Intense = 2,
|
|
471
|
+
Max = 3
|
|
472
|
+
}
|
|
473
|
+
/** ASSUMED from Tasshack — WaterVolume (siid 4 piid 5). NOT YET verified on r2532a. */
|
|
474
|
+
declare enum WaterVolume {
|
|
475
|
+
Low = 1,
|
|
476
|
+
Medium = 2,
|
|
477
|
+
High = 3
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* VERIFIED on r2449a 2026-05-21 — plain 0..3 value space written via
|
|
481
|
+
* CLEAN_MODE_SETTING (siid 2 piid 6). The raw CLEANING_MODE field (siid 4
|
|
482
|
+
* piid 23) packs this in its low 2 bits OR'd with a 0x1400 capability mask;
|
|
483
|
+
* prefer CLEAN_MODE_SETTING to avoid the bitfield trap.
|
|
484
|
+
*/
|
|
485
|
+
declare enum CleaningMode {
|
|
486
|
+
Sweeping = 0,
|
|
487
|
+
Mopping = 1,
|
|
488
|
+
SweepAndMop = 2,
|
|
489
|
+
MopAfterSweep = 3
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Error / fault value space for ERROR (siid 2 piid 2) and FAULTS_STR
|
|
493
|
+
* (siid 4 piid 18). VERIFIED members confirmed live on r2532a; the rest are
|
|
494
|
+
* ASSUMED from Tasshack types.py — the integer is the source of truth.
|
|
495
|
+
*/
|
|
496
|
+
declare enum MiotError {
|
|
497
|
+
Clear = 0,
|
|
498
|
+
WheelRotationAnomaly = 1,
|
|
499
|
+
RobotLifted = 18,
|
|
500
|
+
TaskComplete = 68,
|
|
501
|
+
ManualMopInstallRequired = 74,
|
|
502
|
+
WastewaterTankFull = 105,
|
|
503
|
+
CleanWaterTankEmpty = 107,
|
|
504
|
+
WashboardFilterNeedsCleaning = 114,
|
|
505
|
+
MopPadsMissing = 120,
|
|
506
|
+
BatteryLow = 20,
|
|
507
|
+
ChargeFault = 21,
|
|
508
|
+
BatteryPercentageAnomaly = 22,
|
|
509
|
+
ChargeNoElectric = 28,
|
|
510
|
+
BatteryFault = 29,
|
|
511
|
+
LowBatteryTurnOff = 75,
|
|
512
|
+
RobotStuck = 80,
|
|
513
|
+
RobotStuckRepeat = 81,
|
|
514
|
+
RobotStuck2 = 90,
|
|
515
|
+
RobotStuckOnTables = 91,
|
|
516
|
+
RobotStuckOnPassage = 92,
|
|
517
|
+
RobotStuckOnThreshold = 93,
|
|
518
|
+
RobotStuckOnLowLyingArea = 94,
|
|
519
|
+
RobotStuckOnRamp = 95,
|
|
520
|
+
RobotStuckOnObstacle = 96,
|
|
521
|
+
RobotStuckOnPet = 97,
|
|
522
|
+
RobotStuckOnSlipperySurface = 98,
|
|
523
|
+
RobotStuckOnCarpet = 99,
|
|
524
|
+
RobotStuckOnCurtain = 200,
|
|
525
|
+
BinFull = 101,
|
|
526
|
+
StationDisconnected = 117,
|
|
527
|
+
DustBagFull = 121
|
|
528
|
+
}
|
|
529
|
+
/** VERIFIED on r2532a 2026-05-02 — TASK_STATUS (siid 4 piid 1). */
|
|
530
|
+
declare enum TaskStatus {
|
|
531
|
+
InterruptedOrPaused = 1,
|
|
532
|
+
Active = 2,
|
|
533
|
+
Transitioning = 3,
|
|
534
|
+
OnDockIdle = 6,
|
|
535
|
+
TransientPauseEdge = 12,
|
|
536
|
+
NeedsIntervention = 14
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Per-model vacuum capability records (ported from malard/node-dreame
|
|
541
|
+
* src/capabilities.ts, MIT — attribution retained) plus a resolver that
|
|
542
|
+
* adapts the rich record into the generic CapabilityResolver the BaseDevice
|
|
543
|
+
* expects. r2538z is added mirroring r2532a (X50 sibling), marked unverified
|
|
544
|
+
* until live-confirmed.
|
|
545
|
+
*/
|
|
546
|
+
|
|
547
|
+
/** Rich vacuum capability record. `canX` = behaviour, `hasX` = hardware. */
|
|
548
|
+
interface VacuumCapabilities {
|
|
549
|
+
model: string;
|
|
550
|
+
/** true when the record came from the curated table (false = fallback / assumed). */
|
|
551
|
+
verified: boolean;
|
|
552
|
+
canMop: boolean;
|
|
553
|
+
canAutoInstallMop: boolean;
|
|
554
|
+
hasSideBrush: boolean;
|
|
555
|
+
hasCamera: boolean;
|
|
556
|
+
hasCarpetSensor: boolean;
|
|
557
|
+
hasAiObstacleDetection: boolean;
|
|
558
|
+
canAutoEmpty: boolean;
|
|
559
|
+
canMopWash: boolean;
|
|
560
|
+
canMopDry: boolean;
|
|
561
|
+
canHeatMopWater: boolean;
|
|
562
|
+
hasDetergentReservoir: boolean;
|
|
563
|
+
canCleanPerRoom: boolean;
|
|
564
|
+
supportsVirtualWalls: boolean;
|
|
565
|
+
supportsNoGoZones: boolean;
|
|
566
|
+
hasChildLock: boolean;
|
|
567
|
+
supportsMultiFloor: boolean;
|
|
568
|
+
/** Decodes/fetches the binary live/saved map (envelope → pixel grid → segments). */
|
|
569
|
+
canMap: boolean;
|
|
570
|
+
supportedSuctionLevels: readonly SuctionLevel[];
|
|
571
|
+
supportedWaterVolumes: readonly WaterVolume[];
|
|
572
|
+
}
|
|
573
|
+
declare const MODEL_CAPABILITIES$1: Readonly<Record<string, VacuumCapabilities>>;
|
|
574
|
+
/** Resolve a model to its rich vacuum capability record (frozen / fallback). */
|
|
575
|
+
declare function getVacuumCapabilities(model: string): VacuumCapabilities;
|
|
576
|
+
/** Resolver that the BaseDevice accepts via BaseDeviceInput.capabilities. */
|
|
577
|
+
declare class VacuumCapabilityResolver implements CapabilityResolver {
|
|
578
|
+
resolve(model: string): DeviceCapabilities;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Public types for the live-map decoder.
|
|
583
|
+
*
|
|
584
|
+
* All coordinates are millimetres in the device's world frame, exactly as
|
|
585
|
+
* Dreame's binary encodes them — no Y-flip, no centimetre rescale, no
|
|
586
|
+
* origin shift. The consumer is expected to apply whichever transform
|
|
587
|
+
* fits its rendering library. This is a deliberate departure from the
|
|
588
|
+
* Valetudo schema; see `docs/live-map-format.md` for the rationale.
|
|
589
|
+
*/
|
|
590
|
+
/** Frame type byte from the 27-byte map header. */
|
|
591
|
+
type MapFrameType = 'I' | 'P' | 'W';
|
|
592
|
+
/** Cleaning-path op classes — the four `tr` regex ops collapsed into named runs. */
|
|
593
|
+
type MapPathType = 'mop' | 'sweep' | 'sweep-and-mop' | 'line';
|
|
594
|
+
/**
|
|
595
|
+
* Pixel-grid layer kind. `wall`, `floor`, and `segment` are mutually
|
|
596
|
+
* exclusive primary classifications — one per pixel. `carpet` is an
|
|
597
|
+
* independent overlay (low-bits=11 in fsm:1 path B): a carpet pixel
|
|
598
|
+
* also has a primary classification, and the renderer paints the carpet
|
|
599
|
+
* texture on top of whatever colour the underlying layer chose.
|
|
600
|
+
*/
|
|
601
|
+
type MapLayerType = 'wall' | 'floor' | 'segment' | 'carpet';
|
|
602
|
+
interface MapPose {
|
|
603
|
+
/** mm, world-frame. */
|
|
604
|
+
x: number;
|
|
605
|
+
/** mm, world-frame. */
|
|
606
|
+
y: number;
|
|
607
|
+
/** Degrees. Raw — the renderer applies `VacuumMap.rotation` if it cares. */
|
|
608
|
+
angle: number;
|
|
609
|
+
}
|
|
610
|
+
interface MapDimensions {
|
|
611
|
+
/** mm, world-x of pixel column 0. */
|
|
612
|
+
left: number;
|
|
613
|
+
/** mm, world-y of pixel row 0. */
|
|
614
|
+
top: number;
|
|
615
|
+
/** Pixels. */
|
|
616
|
+
width: number;
|
|
617
|
+
/** Pixels. */
|
|
618
|
+
height: number;
|
|
619
|
+
/** mm per pixel. */
|
|
620
|
+
gridSize: number;
|
|
621
|
+
}
|
|
622
|
+
interface MapBoundingBox {
|
|
623
|
+
/** mm. */
|
|
624
|
+
xMin: number;
|
|
625
|
+
/** mm. */
|
|
626
|
+
yMin: number;
|
|
627
|
+
/** mm. */
|
|
628
|
+
xMax: number;
|
|
629
|
+
/** mm. */
|
|
630
|
+
yMax: number;
|
|
631
|
+
}
|
|
632
|
+
interface MapPoint {
|
|
633
|
+
/** mm. */
|
|
634
|
+
x: number;
|
|
635
|
+
/** mm. */
|
|
636
|
+
y: number;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* One run of consecutive same-class pixels on a single row, encoded as
|
|
640
|
+
* `[xPixel, yPixel, length]` in pixel-space (multiply by `gridSize` and
|
|
641
|
+
* add `left`/`top` to project to mm world-frame).
|
|
642
|
+
*/
|
|
643
|
+
type MapRun = [number, number, number];
|
|
644
|
+
interface MapLayer {
|
|
645
|
+
type: MapLayerType;
|
|
646
|
+
/** Set when `type === "segment"`. Range 1..63. */
|
|
647
|
+
segmentId?: number;
|
|
648
|
+
readonly runs: readonly MapRun[];
|
|
649
|
+
}
|
|
650
|
+
interface MapSegment {
|
|
651
|
+
/** Segment id from the pixel grid (1..63). */
|
|
652
|
+
id: number;
|
|
653
|
+
/**
|
|
654
|
+
* User-given room name. Already decoded from the wire format's
|
|
655
|
+
* base64 — use as-is, do NOT double-decode.
|
|
656
|
+
*
|
|
657
|
+
* `null` when `seg_inf.<id>.name` was missing entirely. Empty-string
|
|
658
|
+
* (`""`) when the user has not named the room — observed live on
|
|
659
|
+
* r2532a 2026-05-07 when no rooms were named in the Dreamehome app.
|
|
660
|
+
* Renderers that fall back via `s.name || \`Room ${s.id}\`` work
|
|
661
|
+
* correctly for both because both are falsy in JS, but a
|
|
662
|
+
* strict-null-only check (`s.name === null ? … : s.name`) would
|
|
663
|
+
* render visible empty labels.
|
|
664
|
+
*/
|
|
665
|
+
name: string | null;
|
|
666
|
+
/** mm, world-frame, derived from the pixel scan. */
|
|
667
|
+
bbox: MapBoundingBox;
|
|
668
|
+
/** mm, world-frame — useful as a label-anchor point. */
|
|
669
|
+
centroid: MapPoint;
|
|
670
|
+
/** Adjacent segment ids from `seg_inf.<id>.nei_id`. */
|
|
671
|
+
readonly neighbours: readonly number[];
|
|
672
|
+
/** Floor material code from `seg_inf.<id>.material`. */
|
|
673
|
+
floorMaterial: number | null;
|
|
674
|
+
/** Floor direction code from `seg_inf.<id>.direction`. */
|
|
675
|
+
floorDirection: number | null;
|
|
676
|
+
/** Whether this segment is in the current cleaning set (`sa`). */
|
|
677
|
+
active: boolean;
|
|
678
|
+
}
|
|
679
|
+
interface MapPath {
|
|
680
|
+
type: MapPathType;
|
|
681
|
+
/** mm, world-frame. */
|
|
682
|
+
readonly points: readonly MapPoint[];
|
|
683
|
+
}
|
|
684
|
+
interface MapObstacle {
|
|
685
|
+
/** Per-obstacle id from `ai_obstacle`. */
|
|
686
|
+
id: number;
|
|
687
|
+
/** mm, world-frame. */
|
|
688
|
+
x: number;
|
|
689
|
+
/** mm, world-frame. */
|
|
690
|
+
y: number;
|
|
691
|
+
/**
|
|
692
|
+
* `ObstacleType` enum value — carried through unchanged. The browser
|
|
693
|
+
* decodes the integer to a label; node-dreame doesn't ship the lookup
|
|
694
|
+
* table because Dreame revises it per firmware.
|
|
695
|
+
*/
|
|
696
|
+
type: number;
|
|
697
|
+
/** 0..100 — Dreame's own confidence percentage. */
|
|
698
|
+
confidence: number;
|
|
699
|
+
/** When the device captured a photo of the obstacle, the OSS file name. */
|
|
700
|
+
photoFileName: string | null;
|
|
701
|
+
/** AES key for photo decryption — separate from the map blob's key. */
|
|
702
|
+
photoKey: string | null;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* One user-defined wall-shaped piece of geometry — a line segment in
|
|
706
|
+
* mm world-frame. Includes both classic virtual walls (`vw.line`) and
|
|
707
|
+
* the X50's threshold variants (`vws.vwsl` / `vws.npthrsd`); the
|
|
708
|
+
* `kind` and `passable` fields discriminate.
|
|
709
|
+
*
|
|
710
|
+
* `kind` defaults to `"wall"` when absent — older callers that
|
|
711
|
+
* pre-date the threshold split don't have to update.
|
|
712
|
+
*
|
|
713
|
+
* Threshold semantics (Tasshack `dev` `map.py:4678-4691`):
|
|
714
|
+
* - `kind: "threshold", passable: true` — passable threshold (X50 firmware
|
|
715
|
+
* where the user has separately configured the impassable set)
|
|
716
|
+
* - `kind: "threshold", passable: false` — impassable threshold
|
|
717
|
+
* - `kind: "threshold"` (no passable) — "virtual" threshold from
|
|
718
|
+
* older firmware that doesn't split passable/impassable
|
|
719
|
+
* - `kind: "wall"` (or absent) — classic virtual wall (`vw.line`)
|
|
720
|
+
*/
|
|
721
|
+
interface MapVirtualWall {
|
|
722
|
+
from: MapPoint;
|
|
723
|
+
to: MapPoint;
|
|
724
|
+
/** Defaults to `"wall"` when absent. */
|
|
725
|
+
kind?: 'wall' | 'threshold';
|
|
726
|
+
/** Only meaningful when `kind === "threshold"`. */
|
|
727
|
+
passable?: boolean;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* One axis-aligned restricted area — either a no-go zone (`vw.rect`,
|
|
731
|
+
* `kind: "noGo"`) or a no-mop zone (`vw.mop`, `kind: "noMop"`).
|
|
732
|
+
*
|
|
733
|
+
* The wire format carries only two opposing corners; this struct
|
|
734
|
+
* normalises them into a `MapBoundingBox`. The optional `angle`
|
|
735
|
+
* mirrors a fifth element Dreame sometimes appends (rotation hint —
|
|
736
|
+
* the rectangle itself remains axis-aligned in the wire format).
|
|
737
|
+
*/
|
|
738
|
+
interface MapRestrictedArea {
|
|
739
|
+
kind: 'noGo' | 'noMop';
|
|
740
|
+
bbox: MapBoundingBox;
|
|
741
|
+
/** Optional rotation hint from the wire format — degrees, may be undefined. */
|
|
742
|
+
angle?: number;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* One wall segment from the per-room wall geometry — present on
|
|
746
|
+
* saved maps as `walls_info.storeys[*].rooms[*].walls[*]`.
|
|
747
|
+
*
|
|
748
|
+
* `type` discriminates the wall variant; observed values on r2532a:
|
|
749
|
+
* `0` — solid wall
|
|
750
|
+
* `1` — opening / doorway
|
|
751
|
+
* Surfaced as-emitted; consumers can ignore or render selectively.
|
|
752
|
+
*
|
|
753
|
+
* `normal` is the unit-vector pointing into the room's interior on
|
|
754
|
+
* the wire (each component typically `-1`, `0`, or `1`).
|
|
755
|
+
*/
|
|
756
|
+
interface MapRoomWall {
|
|
757
|
+
type: number;
|
|
758
|
+
from: MapPoint;
|
|
759
|
+
to: MapPoint;
|
|
760
|
+
normal: MapPoint;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* One room's wall list inside `walls_info.storeys[*].rooms[*]`.
|
|
764
|
+
* `roomId` matches a segment id from the pixel grid where present
|
|
765
|
+
* (some captures show roomIds outside the 1..63 segment range, so do
|
|
766
|
+
* NOT assume a 1:1 mapping at the consumer layer).
|
|
767
|
+
*/
|
|
768
|
+
interface MapRoom {
|
|
769
|
+
roomId: number;
|
|
770
|
+
walls: readonly MapRoomWall[];
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* One floor's worth of rooms inside `walls_info.storeys[*]`.
|
|
774
|
+
* Single-floor homes have exactly one storey; multi-floor would have
|
|
775
|
+
* one entry per floor. `VacuumMap.wallsInfo` carries only the storey
|
|
776
|
+
* matching the current `mapId`.
|
|
777
|
+
*/
|
|
778
|
+
interface MapStorey {
|
|
779
|
+
rooms: readonly MapRoom[];
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Per-room wall geometry from the saved-map blob's `walls_info`
|
|
783
|
+
* field. Big and only present on saved maps (not live I-frames).
|
|
784
|
+
*
|
|
785
|
+
* `versionFlag` is the wire's `version_flag` int — surfaced
|
|
786
|
+
* unchanged in case the schema evolves.
|
|
787
|
+
*/
|
|
788
|
+
interface MapWallsInfo {
|
|
789
|
+
versionFlag: number;
|
|
790
|
+
storeys: readonly MapStorey[];
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* One user-defined low-clearance "sneak under furniture" zone — the
|
|
794
|
+
* X50 retracts its tower while passing through. Surfaced from the
|
|
795
|
+
* tail's `sneak_areas` / `sneak_areas_end` field (Tasshack `dev`
|
|
796
|
+
* `map.py:4776-4809`).
|
|
797
|
+
*
|
|
798
|
+
* The wire format is a polygon: `roi` is an even-length list of
|
|
799
|
+
* alternating `x0, y0, x1, y1, …` ints in mm world-frame. On
|
|
800
|
+
* r2532a 2026-05-07 every observed entry was a 4-corner rect (8
|
|
801
|
+
* ints) — but the Tasshack reference parses arbitrary even lengths,
|
|
802
|
+
* so we surface the points as-emitted rather than coercing to a
|
|
803
|
+
* bounding box.
|
|
804
|
+
*/
|
|
805
|
+
interface MapLowLyingArea {
|
|
806
|
+
/** Stable id from the wire format (for cross-frame correlation). */
|
|
807
|
+
id: number;
|
|
808
|
+
/** Polygon vertices in mm, world-frame. */
|
|
809
|
+
points: readonly MapPoint[];
|
|
810
|
+
/** Floor area in m² — present when the device emitted `sneak_areas_end`. */
|
|
811
|
+
area?: number;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Cleaned-area overlay decoded from the JSON tail's `decmap` field.
|
|
815
|
+
*
|
|
816
|
+
* `decmap` is a recursive blob — a full inner map envelope (header +
|
|
817
|
+
* zlib + JSON tail) embedded as a base64 string in the parent tail.
|
|
818
|
+
* Its pixel grid uses only the low 2 bits (`& 0x03`): `1 = cleaned`,
|
|
819
|
+
* `2 = dirty`. The inner grid has its own dimensions, independent of
|
|
820
|
+
* the parent map; the renderer reprojects onto the parent's pixel
|
|
821
|
+
* grid using the dimensions below.
|
|
822
|
+
*
|
|
823
|
+
* Both `cleaned` and `dirty` are run-length encoded the same way as
|
|
824
|
+
* `MapLayer.runs` — `[xPixel, yPixel, length]` in the inner grid's
|
|
825
|
+
* pixel-space.
|
|
826
|
+
*
|
|
827
|
+
* `cleanedSegments` carries the inner tail's `CleanArea` field when
|
|
828
|
+
* present (per-segment cleaned-area stats). Shape varies per firmware
|
|
829
|
+
* so it's surfaced as opaque.
|
|
830
|
+
*/
|
|
831
|
+
interface MapCleanedAreaOverlay {
|
|
832
|
+
/** Inner blob's own dimensions — independent of the parent map's. */
|
|
833
|
+
dimensions: MapDimensions;
|
|
834
|
+
/** Pixels marked `cleaned` (low-bits == 1) in the inner grid. */
|
|
835
|
+
readonly cleaned: readonly MapRun[];
|
|
836
|
+
/** Pixels marked `dirty` (low-bits == 2) in the inner grid. */
|
|
837
|
+
readonly dirty: readonly MapRun[];
|
|
838
|
+
/** Optional per-segment cleaned-area stats from the inner JSON tail. */
|
|
839
|
+
cleanedSegments?: unknown;
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* The decoded map. Coordinates throughout are mm in the device's world
|
|
843
|
+
* frame. The renderer transforms once when projecting onto its own
|
|
844
|
+
* canvas — see `dimensions` for the parent grid origin / scale.
|
|
845
|
+
*/
|
|
846
|
+
interface VacuumMap {
|
|
847
|
+
mapId: number;
|
|
848
|
+
frameId: number;
|
|
849
|
+
frameType: MapFrameType;
|
|
850
|
+
/** ms since epoch, from JSON tail's `timestamp_ms`. */
|
|
851
|
+
timestamp: number;
|
|
852
|
+
/** Degrees from JSON tail's `mra` — applied by the renderer if it rotates. */
|
|
853
|
+
rotation: number;
|
|
854
|
+
dimensions: MapDimensions;
|
|
855
|
+
robot: MapPose | null;
|
|
856
|
+
dock: MapPose | null;
|
|
857
|
+
docked: boolean;
|
|
858
|
+
readonly layers: readonly MapLayer[];
|
|
859
|
+
readonly segments: readonly MapSegment[];
|
|
860
|
+
readonly paths: readonly MapPath[];
|
|
861
|
+
readonly obstacles: readonly MapObstacle[];
|
|
862
|
+
/**
|
|
863
|
+
* Line-segment walls and threshold variants. Empty array when none
|
|
864
|
+
* configured. Each entry's `kind` (defaults to `"wall"` when absent)
|
|
865
|
+
* and `passable` discriminate classic virtual walls (`vw.line`)
|
|
866
|
+
* from passable / impassable / virtual thresholds (`vws.vwsl` /
|
|
867
|
+
* `vws.npthrsd`). See `MapVirtualWall`.
|
|
868
|
+
*/
|
|
869
|
+
readonly virtualWalls: readonly MapVirtualWall[];
|
|
870
|
+
/**
|
|
871
|
+
* Axis-aligned restricted areas — both no-go (`vw.rect`, `vw.nocpt`)
|
|
872
|
+
* and no-mop (`vw.mop`).
|
|
873
|
+
*/
|
|
874
|
+
readonly restrictedAreas: readonly MapRestrictedArea[];
|
|
875
|
+
/**
|
|
876
|
+
* Low-clearance "sneak under furniture" zones from `sneak_areas` /
|
|
877
|
+
* `sneak_areas_end`. Empty array when none configured.
|
|
878
|
+
*/
|
|
879
|
+
readonly lowLyingAreas: readonly MapLowLyingArea[];
|
|
880
|
+
/**
|
|
881
|
+
* Per-room wall geometry from the saved-map blob's `walls_info`.
|
|
882
|
+
* `null` on live I-frames that don't carry the field — only
|
|
883
|
+
* populated when the parent frame has a saved-map blob to mine
|
|
884
|
+
* from (or is itself the saved-map blob).
|
|
885
|
+
*/
|
|
886
|
+
readonly wallsInfo: MapWallsInfo | null;
|
|
887
|
+
/**
|
|
888
|
+
* Cleaning progress map embedded in the parent frame, decoded from
|
|
889
|
+
* the recursive `decmap` blob. `null` when the parent didn't carry
|
|
890
|
+
* one (typical for live-stream frames; the device emits `decmap`
|
|
891
|
+
* mainly on full-snapshot pushes).
|
|
892
|
+
*/
|
|
893
|
+
cleanedArea: MapCleanedAreaOverlay | null;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Raster PNG renderer for a decoded `VacuumMap`.
|
|
898
|
+
*
|
|
899
|
+
* node-dreame ships only the structured model (no image); this is NEW. We paint
|
|
900
|
+
* one pixel per grid cell (× an optional integer upscale `scale`) from the
|
|
901
|
+
* run-length `layers`, later layers painting over earlier ones. The background
|
|
902
|
+
* is transparent. Per-segment colours come from a deterministic golden-angle
|
|
903
|
+
* palette so the same room id always renders the same hue.
|
|
904
|
+
*
|
|
905
|
+
* Pure: only `pngjs` + arithmetic. No casts.
|
|
906
|
+
*/
|
|
907
|
+
|
|
908
|
+
interface RenderVacuumPngOptions {
|
|
909
|
+
/** Integer upscale factor (nearest-neighbour). Default 1. */
|
|
910
|
+
scale?: number;
|
|
911
|
+
}
|
|
912
|
+
declare function renderVacuumPng(map: VacuumMap, opts?: RenderVacuumPngOptions): Buffer;
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Resolve a Dreame OSS object name (advertised via the PATH push,
|
|
916
|
+
* `siid 6 piid 3`) to a signed download URL and fetch the blob.
|
|
917
|
+
*
|
|
918
|
+
* Endpoint: `POST /dreame-user-iot/iotfile/getDownloadUrl`
|
|
919
|
+
* Body: `{ did, model, filename, region }`
|
|
920
|
+
* Response: `{ code: 0, data: "<signed-url-string>" }` — `data` is the
|
|
921
|
+
* URL string itself, not a nested `{ url }` object.
|
|
922
|
+
*
|
|
923
|
+
* The signed URL is good for ~1 hour (Aliyun signature). We cache it
|
|
924
|
+
* for 30 min by default and on cache hit append `current=<unix_ts>` to
|
|
925
|
+
* bust any intermediate caches (Tasshack convention).
|
|
926
|
+
*
|
|
927
|
+
* v1 only handles live blobs (the I-frame parked in OSS by the device).
|
|
928
|
+
* Permanent saved-map blobs use a different filename mangling — deferred.
|
|
929
|
+
*
|
|
930
|
+
* ADAPTED from node-dreame's `src/map/oss-fetch.ts`: the only changes are
|
|
931
|
+
* import-path rewrites onto our transport (`RequestContext`/`httpPostJson`
|
|
932
|
+
* from `transport/http`, `DreameApiError`/`DreameTransportError` from
|
|
933
|
+
* `transport/errors`, `DreameRegion` from `auth/config`) and typing the
|
|
934
|
+
* injected fetch seam as our `FetchImpl` (undici-backed) rather than the
|
|
935
|
+
* donor's global `typeof fetch` — so the mock type-checks with no cast.
|
|
936
|
+
*/
|
|
937
|
+
|
|
938
|
+
interface OssFetchInput {
|
|
939
|
+
/** Resolved API host (e.g. `eu.iot.dreame.tech:13267`). */
|
|
940
|
+
host: string;
|
|
941
|
+
/** Bearer token from the active `DreameSession`. */
|
|
942
|
+
accessToken: string;
|
|
943
|
+
/** Region — used for header construction and the body's `region` field. */
|
|
944
|
+
region: DreameRegion;
|
|
945
|
+
/** Optional `country` override for headers (defaults from region). */
|
|
946
|
+
country?: string;
|
|
947
|
+
/** Optional `lang` override for headers (defaults from region). */
|
|
948
|
+
lang?: string;
|
|
949
|
+
/** Device id. */
|
|
950
|
+
did: string;
|
|
951
|
+
/** Device model. */
|
|
952
|
+
model: string;
|
|
953
|
+
/** OSS object name (`ali_dreame/<uid>/<did>/<n>`). */
|
|
954
|
+
filename: string;
|
|
955
|
+
/** Caller-supplied AbortSignal — composed with the HTTP timeout. */
|
|
956
|
+
signal?: AbortSignal;
|
|
957
|
+
/** Per-request timeout override in ms. Pass `0` to disable. */
|
|
958
|
+
timeoutMs?: number;
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Minimal fetcher seam consumed by `VacuumDevice.getMap`. Lets callers
|
|
962
|
+
* inject a custom signed-blob fetcher (e.g. a cache-backed or proxied one)
|
|
963
|
+
* without depending on the concrete {@link OssFetcher} class. The real
|
|
964
|
+
* {@link OssFetcher} implements this interface.
|
|
965
|
+
*/
|
|
966
|
+
interface OssFetcherLike {
|
|
967
|
+
fetchBlob(input: OssFetchInput): Promise<Buffer>;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/** Knobs for the segment/zone/spot helpers. Defaults pull from cached state. */
|
|
971
|
+
interface CleanOpts {
|
|
972
|
+
repeats?: number;
|
|
973
|
+
fan?: number;
|
|
974
|
+
water?: number;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* The vacuum widens the {@link BaseDeviceEvents} map with a `'map'` event,
|
|
978
|
+
* emitted by {@link VacuumDevice.getMap} once a fresh map has been decoded.
|
|
979
|
+
* Declaring it here lets `VacuumDevice extends BaseDevice<VacuumDeviceEvents>`
|
|
980
|
+
* type the emitter with no banned cast.
|
|
981
|
+
*/
|
|
982
|
+
type VacuumDeviceEvents = BaseDeviceEvents & {
|
|
983
|
+
map: [VacuumMap];
|
|
984
|
+
};
|
|
985
|
+
/** Input to {@link VacuumDevice.getMap}. */
|
|
986
|
+
interface VacuumGetMapInput {
|
|
987
|
+
/** OSS object name advertised via the PATH push (siid 6 piid 3). */
|
|
988
|
+
filename: string;
|
|
989
|
+
/**
|
|
990
|
+
* Inject the signed-blob fetcher (tests pass a fake). Defaults to a fresh
|
|
991
|
+
* {@link OssFetcher}. Typed as {@link OssFetcherLike} so consumers can pass
|
|
992
|
+
* any object exposing a compatible `fetchBlob`.
|
|
993
|
+
*/
|
|
994
|
+
fetcher?: OssFetcherLike;
|
|
995
|
+
/** Optional AES key for an encrypted blob (hex). */
|
|
996
|
+
key?: string;
|
|
997
|
+
/** Optional AES IV for an encrypted blob (hex). */
|
|
998
|
+
iv?: string;
|
|
999
|
+
/** Override the API host (defaults from the device region). */
|
|
1000
|
+
host?: string;
|
|
1001
|
+
/** Per-request timeout override in ms. */
|
|
1002
|
+
timeoutMs?: number;
|
|
1003
|
+
/** Caller-supplied AbortSignal. */
|
|
1004
|
+
signal?: AbortSignal;
|
|
1005
|
+
}
|
|
1006
|
+
/** A typed Dreame-vacuum handle (state + capability-gated commands). */
|
|
1007
|
+
declare class VacuumDevice extends BaseDevice<VacuumDeviceEvents> {
|
|
1008
|
+
#private;
|
|
1009
|
+
constructor(input: BaseDeviceInput);
|
|
1010
|
+
/** Rich, vacuum-specific capability record (booleans + supported enums). */
|
|
1011
|
+
get vacuumCapabilities(): VacuumCapabilities;
|
|
1012
|
+
get statusRaw(): number | null;
|
|
1013
|
+
get status(): MiotState | null;
|
|
1014
|
+
get battery(): number | null;
|
|
1015
|
+
get chargingRaw(): number | null;
|
|
1016
|
+
get charging(): ChargingStatus | null;
|
|
1017
|
+
get isCharging(): boolean;
|
|
1018
|
+
/** Docked => MiotState Charging or ChargingComplete (on the dock). */
|
|
1019
|
+
get isDocked(): boolean;
|
|
1020
|
+
get suctionRaw(): number | null;
|
|
1021
|
+
get suction(): SuctionLevel | null;
|
|
1022
|
+
get waterRaw(): number | null;
|
|
1023
|
+
get water(): WaterVolume | null;
|
|
1024
|
+
/** Reads the SAFE CLEAN_MODE_SETTING (siid 2 piid 6), plain 0-3. */
|
|
1025
|
+
get cleaningModeRaw(): number | null;
|
|
1026
|
+
get cleaningMode(): CleaningMode | null;
|
|
1027
|
+
get taskStatusRaw(): number | null;
|
|
1028
|
+
get taskStatus(): TaskStatus | null;
|
|
1029
|
+
get errorCode(): number | null;
|
|
1030
|
+
get faults(): readonly number[];
|
|
1031
|
+
get taskProgressPct(): number | null;
|
|
1032
|
+
get mainBrushLeftPct(): number | null;
|
|
1033
|
+
get sideBrushLeftPct(): number | null;
|
|
1034
|
+
get filterLeftPct(): number | null;
|
|
1035
|
+
get volume(): number | null;
|
|
1036
|
+
/**
|
|
1037
|
+
* Start cleaning (MIoT action siid 2 aiid 1). Named `startCleaning` — NOT
|
|
1038
|
+
* `start` — because `BaseDevice.start()` is the lifecycle method that opens
|
|
1039
|
+
* the MQTT push, and the facade relies on that meaning. Donor `node-dreame`
|
|
1040
|
+
* names this `start()` only because its standalone `Vacuum` class owns no
|
|
1041
|
+
* lifecycle method to collide with.
|
|
1042
|
+
*/
|
|
1043
|
+
startCleaning(): Promise<unknown>;
|
|
1044
|
+
pause(): Promise<unknown>;
|
|
1045
|
+
stop(): Promise<unknown>;
|
|
1046
|
+
/** Return to the dock / charge (MIoT action CHARGE, siid 3 aiid 1). */
|
|
1047
|
+
dock(): Promise<unknown>;
|
|
1048
|
+
locate(): Promise<unknown>;
|
|
1049
|
+
clearWarning(): Promise<unknown>;
|
|
1050
|
+
startAutoEmpty(): Promise<unknown>;
|
|
1051
|
+
/** Type-safe suction write. Validates against the model's supported levels. */
|
|
1052
|
+
setSuction(level: SuctionLevel): Promise<unknown>;
|
|
1053
|
+
/**
|
|
1054
|
+
* Untyped suction write: validates an arbitrary number against the model's
|
|
1055
|
+
* supported levels and rejects with `RangeError` on an invalid input. This
|
|
1056
|
+
* is the raw-input entry point so callers (and tests) can exercise validation
|
|
1057
|
+
* with no type assertions. `async` so the guard surfaces as a rejection.
|
|
1058
|
+
*/
|
|
1059
|
+
setSuctionRaw(level: number): Promise<unknown>;
|
|
1060
|
+
/** Type-safe water-volume write. Validates against supported volumes. */
|
|
1061
|
+
setWater(volume: WaterVolume): Promise<unknown>;
|
|
1062
|
+
/** Untyped water-volume write: validates an arbitrary number (zero casts). */
|
|
1063
|
+
setWaterRaw(volume: number): Promise<unknown>;
|
|
1064
|
+
/** SAFE clean-mode write — uses CLEAN_MODE_SETTING (siid 2 piid 6), never the 0x1400 bitfield. */
|
|
1065
|
+
setCleaningMode(mode: CleaningMode): Promise<unknown>;
|
|
1066
|
+
cleanSegments(ids: number[], opts?: CleanOpts): Promise<unknown>;
|
|
1067
|
+
cleanZones(zones: Array<{
|
|
1068
|
+
x0: number;
|
|
1069
|
+
y0: number;
|
|
1070
|
+
x1: number;
|
|
1071
|
+
y1: number;
|
|
1072
|
+
}>, opts?: CleanOpts): Promise<unknown>;
|
|
1073
|
+
/**
|
|
1074
|
+
* Send the robot to clean a single point (the SPOT custom-clean action,
|
|
1075
|
+
* mode 20). ASSUMED: Tasshack's "go to point" is the same SPOT action — this
|
|
1076
|
+
* is exposed as `cleanSpot` because it maps to the spot/custom-clean action,
|
|
1077
|
+
* not a distinct go-to. No separate `goTo` is exposed.
|
|
1078
|
+
*/
|
|
1079
|
+
cleanSpot(point: {
|
|
1080
|
+
x: number;
|
|
1081
|
+
y: number;
|
|
1082
|
+
}, opts?: CleanOpts): Promise<unknown>;
|
|
1083
|
+
/** The most-recently-decoded map, or `null` until {@link getMap} succeeds. */
|
|
1084
|
+
get lastMap(): VacuumMap | null;
|
|
1085
|
+
/**
|
|
1086
|
+
* The current room/segment id, derived from the most-recently-decoded map's
|
|
1087
|
+
* active-segment set (`sa`). `null` when no map has been fetched or no
|
|
1088
|
+
* segment is currently active. REPLACES the P3 "intentionally not exposed"
|
|
1089
|
+
* placeholder — the value now comes from the decoded map layer (P5).
|
|
1090
|
+
*/
|
|
1091
|
+
get currentSegmentId(): number | null;
|
|
1092
|
+
/**
|
|
1093
|
+
* Fetch the current saved/live-map blob (OSS), decode it, cache it as
|
|
1094
|
+
* {@link lastMap}, emit a `'map'` event, and return the {@link VacuumMap}.
|
|
1095
|
+
*
|
|
1096
|
+
* Capability-gated on `canMap`. The `filename` is the OSS object name the
|
|
1097
|
+
* caller resolves from a `mapInfo`/PATH push. The fetcher is injectable so
|
|
1098
|
+
* tests drive it with a synthetic blob and no live network.
|
|
1099
|
+
*
|
|
1100
|
+
* NOTE: active live-frame P-frame STREAMING (continuous merge orchestration)
|
|
1101
|
+
* is a documented follow-up; `getMap` resolves a single frame here. The
|
|
1102
|
+
* `applyVacuumPFrame` merge primitive ships separately for that work.
|
|
1103
|
+
*/
|
|
1104
|
+
getMap(input: VacuumGetMapInput): Promise<VacuumMap>;
|
|
1105
|
+
/** Props worth seeding on start() / polling — exported for the facade. */
|
|
1106
|
+
static readonly DEFAULT_PROPS: readonly [{
|
|
1107
|
+
readonly siid: 2;
|
|
1108
|
+
readonly piid: 1;
|
|
1109
|
+
}, {
|
|
1110
|
+
readonly siid: 2;
|
|
1111
|
+
readonly piid: 2;
|
|
1112
|
+
}, {
|
|
1113
|
+
readonly siid: 4;
|
|
1114
|
+
readonly piid: 18;
|
|
1115
|
+
}, {
|
|
1116
|
+
readonly siid: 4;
|
|
1117
|
+
readonly piid: 1;
|
|
1118
|
+
}, {
|
|
1119
|
+
readonly siid: 4;
|
|
1120
|
+
readonly piid: 4;
|
|
1121
|
+
}, {
|
|
1122
|
+
readonly siid: 4;
|
|
1123
|
+
readonly piid: 5;
|
|
1124
|
+
}, {
|
|
1125
|
+
readonly siid: 2;
|
|
1126
|
+
readonly piid: 6;
|
|
1127
|
+
}, {
|
|
1128
|
+
readonly siid: 4;
|
|
1129
|
+
readonly piid: 63;
|
|
1130
|
+
}, {
|
|
1131
|
+
readonly siid: 3;
|
|
1132
|
+
readonly piid: 1;
|
|
1133
|
+
}, {
|
|
1134
|
+
readonly siid: 3;
|
|
1135
|
+
readonly piid: 2;
|
|
1136
|
+
}, {
|
|
1137
|
+
readonly siid: 9;
|
|
1138
|
+
readonly piid: 2;
|
|
1139
|
+
}, {
|
|
1140
|
+
readonly siid: 10;
|
|
1141
|
+
readonly piid: 2;
|
|
1142
|
+
}, {
|
|
1143
|
+
readonly siid: 11;
|
|
1144
|
+
readonly piid: 1;
|
|
1145
|
+
}, {
|
|
1146
|
+
readonly siid: 7;
|
|
1147
|
+
readonly piid: 1;
|
|
1148
|
+
}];
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Mower MIoT enum value spaces, reverse-engineered from antondaubert/dreame-mower
|
|
1153
|
+
* (const.py DeviceStatus + STATUS_MAPPING + CHARGING_STATUS_MAPPING, and
|
|
1154
|
+
* property/mower_control.py + service5.py). The donor is a working HA
|
|
1155
|
+
* integration, so these are VERIFIED-by-donor; members annotated ASSUMED are
|
|
1156
|
+
* not yet corroborated. The integer is always the source of truth.
|
|
1157
|
+
*/
|
|
1158
|
+
/** VERIFIED-by-donor — STATUS_PROPERTY (siid 2 piid 1), const.py DeviceStatus. */
|
|
1159
|
+
declare enum MowerStatus {
|
|
1160
|
+
NoStatus = 0,
|
|
1161
|
+
Mowing = 1,
|
|
1162
|
+
Standby = 2,
|
|
1163
|
+
Paused = 3,
|
|
1164
|
+
PausedDueToErrors = 4,
|
|
1165
|
+
ReturningToCharge = 5,
|
|
1166
|
+
Charging = 6,
|
|
1167
|
+
Mapping = 11,
|
|
1168
|
+
ChargingComplete = 13,
|
|
1169
|
+
Updating = 14
|
|
1170
|
+
}
|
|
1171
|
+
/** VERIFIED-by-donor — CHARGING_STATUS_PROPERTY (siid 3 piid 2), CHARGING_STATUS_MAPPING. */
|
|
1172
|
+
declare enum MowerChargingStatus {
|
|
1173
|
+
NotDocked = 0,
|
|
1174
|
+
Charging = 1,
|
|
1175
|
+
NotCharging = 2,
|
|
1176
|
+
ChargingCompleted = 3,
|
|
1177
|
+
ReturnToCharge = 5,
|
|
1178
|
+
/** Charging paused: battery temperature too low (donor issue #40). */
|
|
1179
|
+
ChargingPausedLowTemperature = 16
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* VERIFIED-by-donor — per-zone control code in MOWER_CONTROL_STATUS (siid 2
|
|
1183
|
+
* piid 56) `status` array entries `[zone_id, code]`. mower_control.py.
|
|
1184
|
+
*/
|
|
1185
|
+
declare enum MowerControlAction {
|
|
1186
|
+
/** Zone waiting in a multi-zone session. */
|
|
1187
|
+
Queued = -1,
|
|
1188
|
+
/** Actively mowing. */
|
|
1189
|
+
Continue = 0,
|
|
1190
|
+
Completed = 2,
|
|
1191
|
+
Pause = 4
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Observed TASK_STATUS codes (siid 5 piid 104), service5.py TASK_STATUS_MAPPING.
|
|
1195
|
+
* Only `SpotIncomplete` (7) has a confirmed meaning; the others in the donor are
|
|
1196
|
+
* "Unknown task status: N" — ASSUMED / not exposed as named members here.
|
|
1197
|
+
*/
|
|
1198
|
+
declare enum MowerTaskStatus {
|
|
1199
|
+
/** "Task incomplete - spot mowing". */
|
|
1200
|
+
SpotIncomplete = 7
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* Cast-free mower decoders. Re-exports the shared numeric primitives, and adds
|
|
1205
|
+
* the two structured-payload parsers the typed getters consume:
|
|
1206
|
+
* - parseTaskDescriptor : SCHEDULING_TASK (2:50) {t,d:{...}} -> typed task
|
|
1207
|
+
* - parseControlStatus : MOWER_CONTROL_STATUS (2:56) {status:[[zone,code]]}
|
|
1208
|
+
* Ported from antondaubert/dreame-mower property/{scheduling,mower_control}.py.
|
|
1209
|
+
* The binary pose-track geometry (1:4) is OUT OF SCOPE here (P5).
|
|
1210
|
+
*/
|
|
1211
|
+
|
|
1212
|
+
/** Typed scheduling task descriptor (subset of the donor TaskHandler fields). */
|
|
1213
|
+
interface MowerTaskDescriptor {
|
|
1214
|
+
taskType: string;
|
|
1215
|
+
executionActive: boolean;
|
|
1216
|
+
/** `d.o` — coverage target % or mode sentinel. */
|
|
1217
|
+
coverageTarget: number;
|
|
1218
|
+
taskActive: boolean;
|
|
1219
|
+
areaId: number[] | null;
|
|
1220
|
+
regionId: number[] | null;
|
|
1221
|
+
elapsedTime: number | null;
|
|
1222
|
+
}
|
|
1223
|
+
/** Typed mower control status (2:56). */
|
|
1224
|
+
interface MowerControlState {
|
|
1225
|
+
action: MowerControlAction | null;
|
|
1226
|
+
statusCode: number | null;
|
|
1227
|
+
zones: number[][];
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* Per-model mower capability records + a resolver adapting the rich record into
|
|
1232
|
+
* the generic CapabilityResolver the BaseDevice expects (mirrors the vacuum
|
|
1233
|
+
* capability layer). The donor (antondaubert/dreame-mower) has no per-model
|
|
1234
|
+
* capability matrix for mowers, so the targeted-mowing flags are ASSUMED working
|
|
1235
|
+
* hypotheses from the donor command surface; verified:false until live-checked.
|
|
1236
|
+
*/
|
|
1237
|
+
|
|
1238
|
+
/** Rich mower capability record. `canX` = behaviour the model accepts. */
|
|
1239
|
+
interface MowerCapabilities {
|
|
1240
|
+
model: string;
|
|
1241
|
+
/** true only for live-confirmed records (false = assumed / fallback). */
|
|
1242
|
+
verified: boolean;
|
|
1243
|
+
/** Zone-selective mowing (2:50 o:102). */
|
|
1244
|
+
canMowZones: boolean;
|
|
1245
|
+
/** Edge / contour mowing (2:50 o:101). */
|
|
1246
|
+
canMowEdges: boolean;
|
|
1247
|
+
/** Spot mowing (2:50 o:103). */
|
|
1248
|
+
canMowSpots: boolean;
|
|
1249
|
+
/** All-area, map-targeted start (2:50 o:100). */
|
|
1250
|
+
canMowAllArea: boolean;
|
|
1251
|
+
/** Resume after pause (2:50 o:5). Generic to the mower action surface. */
|
|
1252
|
+
canResume: boolean;
|
|
1253
|
+
/** Has a scheduling task descriptor (2:50). */
|
|
1254
|
+
canSchedule: boolean;
|
|
1255
|
+
/** Fetches/parses the batched vector map (MAP.* JSON → zones/paths/contours). */
|
|
1256
|
+
canMap: boolean;
|
|
1257
|
+
}
|
|
1258
|
+
declare const MODEL_CAPABILITIES: Readonly<Record<string, MowerCapabilities>>;
|
|
1259
|
+
/** Resolve a model to its rich mower capability record (frozen / fallback). */
|
|
1260
|
+
declare function getMowerCapabilities(model: string): MowerCapabilities;
|
|
1261
|
+
/** Resolver the BaseDevice accepts via BaseDeviceInput.capabilities. */
|
|
1262
|
+
declare class MowerCapabilityResolver implements CapabilityResolver {
|
|
1263
|
+
resolve(model: string): DeviceCapabilities;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Structured model for the mower vector map.
|
|
1268
|
+
*
|
|
1269
|
+
* PORT of the `@dataclass` definitions in antondaubert/dreame-mower's
|
|
1270
|
+
* `map_data_parser.py` (`MowerZone`, `MowerPath`, `MowerContour`,
|
|
1271
|
+
* `MowerSpotArea`, `MowerAvailableMap`, `MowerMapBoundary`, `MowerMowPath`,
|
|
1272
|
+
* `MowerVectorMap`) re-typed as TypeScript interfaces with camelCase fields.
|
|
1273
|
+
*
|
|
1274
|
+
* Defaults are applied by the parser (Task 5.15), NOT by the type. Coordinate
|
|
1275
|
+
* points are kept as `{ x, y }` objects (not tuples) to mirror the vacuum
|
|
1276
|
+
* map's `MapPoint` and keep the public API ergonomic; the parser converts the
|
|
1277
|
+
* wire `{ x, y }` shape directly.
|
|
1278
|
+
*/
|
|
1279
|
+
/** A single coordinate point in mower map units. */
|
|
1280
|
+
interface MowerPoint {
|
|
1281
|
+
x: number;
|
|
1282
|
+
y: number;
|
|
1283
|
+
}
|
|
1284
|
+
/** A mowing zone defined by a polygon boundary. */
|
|
1285
|
+
interface MowerZone {
|
|
1286
|
+
zoneId: number;
|
|
1287
|
+
path: readonly MowerPoint[];
|
|
1288
|
+
name: string;
|
|
1289
|
+
zoneType: number;
|
|
1290
|
+
shapeType: number;
|
|
1291
|
+
area: number;
|
|
1292
|
+
time: number;
|
|
1293
|
+
etime: number;
|
|
1294
|
+
}
|
|
1295
|
+
/** A spot-mowing area defined by a polygon boundary. */
|
|
1296
|
+
interface MowerSpotArea {
|
|
1297
|
+
areaId: number;
|
|
1298
|
+
path: readonly MowerPoint[];
|
|
1299
|
+
name: string;
|
|
1300
|
+
shapeType: number;
|
|
1301
|
+
area: number;
|
|
1302
|
+
}
|
|
1303
|
+
/** A navigation path between zones. */
|
|
1304
|
+
interface MowerPathEntry {
|
|
1305
|
+
pathId: number;
|
|
1306
|
+
path: readonly MowerPoint[];
|
|
1307
|
+
pathType: number;
|
|
1308
|
+
}
|
|
1309
|
+
/** A contour entry used for boundary or edge mowing. */
|
|
1310
|
+
interface MowerContour {
|
|
1311
|
+
contourId: readonly [number, number];
|
|
1312
|
+
path: readonly MowerPoint[];
|
|
1313
|
+
contourType: number;
|
|
1314
|
+
shapeType: number;
|
|
1315
|
+
}
|
|
1316
|
+
/** Bounding box for the entire map. */
|
|
1317
|
+
interface MowerMapBoundary {
|
|
1318
|
+
x1: number;
|
|
1319
|
+
y1: number;
|
|
1320
|
+
x2: number;
|
|
1321
|
+
y2: number;
|
|
1322
|
+
}
|
|
1323
|
+
/** Mowing-path trace — the actual trail the mower followed. */
|
|
1324
|
+
interface MowerMowPath {
|
|
1325
|
+
zoneId: number;
|
|
1326
|
+
segments: readonly (readonly MowerPoint[])[];
|
|
1327
|
+
}
|
|
1328
|
+
/** A discovered map that can be targeted by map-aware mowing tasks. */
|
|
1329
|
+
interface MowerAvailableMap {
|
|
1330
|
+
mapId: number;
|
|
1331
|
+
mapIndex: number;
|
|
1332
|
+
name: string;
|
|
1333
|
+
totalArea: number;
|
|
1334
|
+
}
|
|
1335
|
+
/** Complete vector map data for a mower, parsed from the batch API. */
|
|
1336
|
+
interface MowerMap {
|
|
1337
|
+
zones: readonly MowerZone[];
|
|
1338
|
+
spotAreas: readonly MowerSpotArea[];
|
|
1339
|
+
forbiddenAreas: readonly MowerZone[];
|
|
1340
|
+
paths: readonly MowerPathEntry[];
|
|
1341
|
+
contours: readonly MowerContour[];
|
|
1342
|
+
boundary: MowerMapBoundary | null;
|
|
1343
|
+
totalArea: number;
|
|
1344
|
+
name: string;
|
|
1345
|
+
mapId: number;
|
|
1346
|
+
mapIndex: number;
|
|
1347
|
+
mowPaths: readonly MowerMowPath[];
|
|
1348
|
+
availableMaps: readonly MowerAvailableMap[];
|
|
1349
|
+
currentMapId: number | null;
|
|
1350
|
+
lastUpdated: number | null;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* Deterministic SVG renderer for a structured `MowerMap`.
|
|
1355
|
+
*
|
|
1356
|
+
* ADAPT of antondaubert/dreame-mower's `svg_map_generator.py` — only the
|
|
1357
|
+
* geometry path is kept. We port `calculate_bounds`, `coord_to_pixel`
|
|
1358
|
+
* (aspect-preserving, Y-flipped, centred), `svg_polygon` (≥3 points),
|
|
1359
|
+
* `svg_path_from_segments` (M/L with consecutive-pixel dedupe), and the
|
|
1360
|
+
* `create_svg_document`/`finish_svg_document` framing.
|
|
1361
|
+
*
|
|
1362
|
+
* The HA-coordinator-coupled chrome (legend, status overlay, timestamp, title,
|
|
1363
|
+
* historical-file handling, rotation-from-coordinator, live-tracking overlay,
|
|
1364
|
+
* mower-position circle) is DROPPED — it depends on a `coordinator`, a file
|
|
1365
|
+
* path and live device state we don't carry. The renderer takes only a
|
|
1366
|
+
* `MowerMap` + optional `{ width, height, padding }`.
|
|
1367
|
+
*
|
|
1368
|
+
* Zone `name`s are NOT rendered as text in v1 (keeps the output injection-safe
|
|
1369
|
+
* and minimal — no untrusted strings reach the SVG). Pure string building, no
|
|
1370
|
+
* deps, cast-free.
|
|
1371
|
+
*/
|
|
1372
|
+
|
|
1373
|
+
interface RenderMowerSvgOptions {
|
|
1374
|
+
/** Canvas width in pixels. Default 1200. */
|
|
1375
|
+
width?: number;
|
|
1376
|
+
/** Canvas height in pixels. Default 1200. */
|
|
1377
|
+
height?: number;
|
|
1378
|
+
/** Padding around the map content in pixels. Default 50. */
|
|
1379
|
+
padding?: number;
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Render a `MowerMap` into a deterministic SVG document string.
|
|
1383
|
+
*
|
|
1384
|
+
* Draw order (back to front): background, nav paths (dashed grey), zone fills
|
|
1385
|
+
* (per-zone pastel), zone outlines, mow-path tracks (orange), forbidden-area
|
|
1386
|
+
* polygons (red). An empty map renders a centred "No map data available"
|
|
1387
|
+
* fallback inside a closed `<svg>`.
|
|
1388
|
+
*/
|
|
1389
|
+
declare function renderMowerSvg(map: MowerMap, opts?: RenderMowerSvgOptions): string;
|
|
1390
|
+
|
|
1391
|
+
/**
|
|
1392
|
+
* Injectable batch-fetch seam. Given a device id + the batch property groups
|
|
1393
|
+
* (e.g. `['MAP','M_PATH']`), resolves the raw `{ 'MAP.0': …, 'MAP.info': … }`
|
|
1394
|
+
* chunk dict the firmware returns. The default wires `getBatchDeviceDatas` from
|
|
1395
|
+
* the cloud layer — but its live endpoint path is NOT yet recovered (see
|
|
1396
|
+
* `cloud/commands.ts`), so production callers MUST inject a working fetcher.
|
|
1397
|
+
* Tests inject a fake with no cast.
|
|
1398
|
+
*/
|
|
1399
|
+
type BatchDeviceDataFetcher = (did: string, props: string[]) => Promise<Record<string, unknown>>;
|
|
1400
|
+
/** Mower-specific construction input: adds the injectable batch-fetch seam. */
|
|
1401
|
+
interface MowerDeviceInput extends BaseDeviceInput {
|
|
1402
|
+
/** Inject the batched device-data fetcher (maps). Tests pass a fake. */
|
|
1403
|
+
getBatchDeviceDatas?: BatchDeviceDataFetcher;
|
|
1404
|
+
}
|
|
1405
|
+
/** A typed Dreame-mower handle (state + capability-gated commands). */
|
|
1406
|
+
declare class MowerDevice extends BaseDevice {
|
|
1407
|
+
#private;
|
|
1408
|
+
constructor(input: MowerDeviceInput);
|
|
1409
|
+
/** Rich, mower-specific capability record. */
|
|
1410
|
+
get mowerCapabilities(): MowerCapabilities;
|
|
1411
|
+
get statusRaw(): number | null;
|
|
1412
|
+
get status(): MowerStatus | null;
|
|
1413
|
+
get battery(): number | null;
|
|
1414
|
+
get chargingRaw(): number | null;
|
|
1415
|
+
get charging(): MowerChargingStatus | null;
|
|
1416
|
+
/** Docked => on the dock (Charging / ChargingComplete state). */
|
|
1417
|
+
get isDocked(): boolean;
|
|
1418
|
+
get isMowing(): boolean;
|
|
1419
|
+
get taskStatusRaw(): number | null;
|
|
1420
|
+
/** Parsed scheduling task descriptor (2:50), or null. */
|
|
1421
|
+
get task(): MowerTaskDescriptor | null;
|
|
1422
|
+
/**
|
|
1423
|
+
* Mowing coverage target / progress signal from the task descriptor (`d.o`).
|
|
1424
|
+
* This is the P4 progress surface; the byte-accurate pose-track % is P5.
|
|
1425
|
+
*/
|
|
1426
|
+
get coverageTargetPct(): number | null;
|
|
1427
|
+
/** Parsed per-zone control status (2:56), or null. */
|
|
1428
|
+
get controlStatus(): MowerControlState | null;
|
|
1429
|
+
get controlAction(): MowerControlAction | null;
|
|
1430
|
+
/**
|
|
1431
|
+
* Start the generic mowing action (siid 5 aiid 1). Named `startMowing` — NOT
|
|
1432
|
+
* `start` — because `BaseDevice.start()` is the MQTT lifecycle method the
|
|
1433
|
+
* facade relies on; overriding it would break handle startup.
|
|
1434
|
+
*/
|
|
1435
|
+
startMowing(): Promise<unknown>;
|
|
1436
|
+
pause(): Promise<unknown>;
|
|
1437
|
+
stop(): Promise<unknown>;
|
|
1438
|
+
/** Send the mower to its dock (siid 5 aiid 3). */
|
|
1439
|
+
dock(): Promise<unknown>;
|
|
1440
|
+
/**
|
|
1441
|
+
* Resume mowing after a pause. Encoded as the continueControl opcode
|
|
1442
|
+
* `{m:'a', p:0, o:5}` sent as the single in-param of the SCHEDULING_TASK
|
|
1443
|
+
* (2:50) action — NOT a siid-5 action. Mirrors the donor TASK_PAYLOAD_RESUME.
|
|
1444
|
+
*/
|
|
1445
|
+
resume(): Promise<unknown>;
|
|
1446
|
+
/** All-area, map-targeted mowing (2:50 o:100). */
|
|
1447
|
+
startMowingAllArea(mapId: number): Promise<unknown>;
|
|
1448
|
+
/** Zone-selective mowing (2:50 o:102). */
|
|
1449
|
+
startMowingZones(zoneIds: number[]): Promise<unknown>;
|
|
1450
|
+
/** Edge / contour mowing (2:50 o:101). Contour ids are two-int pairs [[1,0]]. */
|
|
1451
|
+
startMowingEdges(contourIds: number[][]): Promise<unknown>;
|
|
1452
|
+
/** Spot mowing (2:50 o:103). */
|
|
1453
|
+
startMowingSpots(spotAreaIds: number[]): Promise<unknown>;
|
|
1454
|
+
/** The most-recently-parsed map, or `null` until {@link getMap} succeeds. */
|
|
1455
|
+
get lastMap(): MowerMap | null;
|
|
1456
|
+
/**
|
|
1457
|
+
* Fetch the batched vector-map data, parse it into a {@link MowerMap}, cache
|
|
1458
|
+
* it as {@link lastMap}, and return it. Capability-gated on `canMap`.
|
|
1459
|
+
*
|
|
1460
|
+
* The batch fetcher is injected at construction (the live cloud endpoint path
|
|
1461
|
+
* is not yet recovered — see `cloud/commands.ts`). Rejects with
|
|
1462
|
+
* {@link DreameError} if no fetcher was injected or the batch yields no
|
|
1463
|
+
* parseable map (asleep mower / empty data).
|
|
1464
|
+
*/
|
|
1465
|
+
getMap(props?: readonly string[]): Promise<MowerMap>;
|
|
1466
|
+
/**
|
|
1467
|
+
* Render the current map to a deterministic SVG string. Uses {@link lastMap}
|
|
1468
|
+
* when present, otherwise fetches first via {@link getMap}.
|
|
1469
|
+
*/
|
|
1470
|
+
mapSvg(opts?: RenderMowerSvgOptions): Promise<string>;
|
|
1471
|
+
/** Props worth seeding on start() / polling — exported for the facade. */
|
|
1472
|
+
static readonly DEFAULT_PROPS: readonly [{
|
|
1473
|
+
readonly siid: 2;
|
|
1474
|
+
readonly piid: 1;
|
|
1475
|
+
}, {
|
|
1476
|
+
readonly siid: 3;
|
|
1477
|
+
readonly piid: 1;
|
|
1478
|
+
}, {
|
|
1479
|
+
readonly siid: 3;
|
|
1480
|
+
readonly piid: 2;
|
|
1481
|
+
}, {
|
|
1482
|
+
readonly siid: 5;
|
|
1483
|
+
readonly piid: 104;
|
|
1484
|
+
}, {
|
|
1485
|
+
readonly siid: 2;
|
|
1486
|
+
readonly piid: 50;
|
|
1487
|
+
}, {
|
|
1488
|
+
readonly siid: 2;
|
|
1489
|
+
readonly piid: 56;
|
|
1490
|
+
}];
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
export { BaseDevice, type BaseDeviceEvents, type BatchDeviceDataFetcher, type CapabilityResolver, ChargingStatus, type CleanOpts, CleaningMode, type CreateDeviceArgs, DefaultCapabilityResolver, type DeviceCapabilities, type DeviceEvent, DreameApiError, DreameAuthError, type DreameCloudState, type DreameDevice, DreameDeviceOfflineError, DreameError, type DreameRegion, type DreameSession, DreameTransportError, LIBRARY_NAME, MODEL_CAPABILITIES as MOWER_MODEL_CAPABILITIES, type MapBoundingBox, type MapCleanedAreaOverlay, type MapDimensions, type MapFrameType, type MapLayer, type MapLayerType, type MapLowLyingArea, type MapObstacle, type MapPath, type MapPathType, type MapPoint, type MapPose, type MapRestrictedArea, type MapRoom, type MapRoomWall, type MapRun, type MapSegment, type MapStorey, type MapVirtualWall, type MapWallsInfo, type MiotAction, MiotError, type MiotProp, MiotState, type MowerAvailableMap, type MowerCapabilities, MowerCapabilityResolver, MowerChargingStatus, type MowerContour, MowerControlAction, type MowerControlState, MowerDevice, type MowerDeviceInput, type MowerMap, type MowerMapBoundary, type MowerMowPath, type MowerPathEntry, type MowerPoint, type MowerSpotArea, MowerStatus, type MowerTaskDescriptor, MowerTaskStatus, type MowerZone, Nodreame, type NodreameDeps, type NodreameEvents, type NodreameOptions, type OssFetchInput, type OssFetcherLike, type PropertyChangedEvent, type PropertyResult, type PropertyState, type PropertyWrite, type RenderMowerSvgOptions, type RenderVacuumPngOptions, type StateChangedEvent, SuctionLevel, TaskStatus, MODEL_CAPABILITIES$1 as VACUUM_MODEL_CAPABILITIES, type VacuumCapabilities, VacuumCapabilityResolver, VacuumDevice, type VacuumGetMapInput, type VacuumMap, WaterVolume, getMowerCapabilities, getVacuumCapabilities, renderMowerSvg, renderVacuumPng, resolveCapabilities };
|