@apocaliss92/nodedreame 1.2.0 → 1.3.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/README.md +48 -0
- package/dist/index.cjs +434 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +337 -1
- package/dist/index.d.ts +337 -1
- package/dist/index.js +432 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -288,6 +288,54 @@ parser and SVG renderer from
|
|
|
288
288
|
[antondaubert/dreame-mower](https://github.com/antondaubert/dreame-mower). Both
|
|
289
289
|
are MIT — see `LICENSE`.
|
|
290
290
|
|
|
291
|
+
## Diagnostic dump (read-only)
|
|
292
|
+
|
|
293
|
+
`nodedreame` can record what a device exposes while it operates and export an
|
|
294
|
+
**anonymized** JSON you can attach to a GitHub issue to help map undocumented
|
|
295
|
+
codes (e.g. mower `taskStatus` 2/3/10/13). The dumper is strictly **read-only** —
|
|
296
|
+
it never sends a command and never wakes the robot to act. It only subscribes to
|
|
297
|
+
the device's event stream (`on`/`off`), reads the cache (`getProperty`), and
|
|
298
|
+
pulls the cloud shadow (`refreshFromCache`).
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
import { Nodreame, createDumper } from '@apocaliss92/nodedreame';
|
|
302
|
+
|
|
303
|
+
const client = new Nodreame({ username, password, region: 'eu' });
|
|
304
|
+
await client.login();
|
|
305
|
+
const [device] = await client.discoverDevices();
|
|
306
|
+
|
|
307
|
+
const dumper = createDumper(device);
|
|
308
|
+
await dumper.start(); // hooks the live stream + periodic cloud-shadow read
|
|
309
|
+
// ...operate the device normally for a few minutes...
|
|
310
|
+
await dumper.stop();
|
|
311
|
+
|
|
312
|
+
console.log(dumper.exportJson()); // pretty, anonymized — safe to share
|
|
313
|
+
await client.close();
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
What it captures: per-property distinct value-sets with an `unmapped` flag
|
|
317
|
+
(values that match no known enum — the highest-value signal), MIoT events, and a
|
|
318
|
+
static command/capability catalog. What it strips: device/account ids, tokens,
|
|
319
|
+
MAC, serial, Wi-Fi/IP, GPS, room names, and any custom device name (all →
|
|
320
|
+
`[redacted]`). `firmware`/`region` are omitted (not surfaced by the device
|
|
321
|
+
handle yet).
|
|
322
|
+
|
|
323
|
+
Dump a whole account at once with `createClientDumper(client)`, which returns one
|
|
324
|
+
dumper per discovered device:
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
import { createClientDumper } from '@apocaliss92/nodedreame';
|
|
328
|
+
|
|
329
|
+
const dumpers = createClientDumper(client);
|
|
330
|
+
await Promise.all(dumpers.map((d) => d.start()));
|
|
331
|
+
// ...observe...
|
|
332
|
+
await Promise.all(dumpers.map((d) => d.stop()));
|
|
333
|
+
const dumps = dumpers.map((d) => d.exportJson());
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Share the exported JSON in a library issue — maintainers diff it against the enum
|
|
337
|
+
tables to label new codes and fold them into the library.
|
|
338
|
+
|
|
291
339
|
## Install
|
|
292
340
|
|
|
293
341
|
```bash
|
package/dist/index.cjs
CHANGED
|
@@ -57,6 +57,8 @@ __export(index_exports, {
|
|
|
57
57
|
VacuumCapabilityResolver: () => VacuumCapabilityResolver,
|
|
58
58
|
VacuumDevice: () => VacuumDevice,
|
|
59
59
|
WaterVolume: () => WaterVolume,
|
|
60
|
+
createClientDumper: () => createClientDumper,
|
|
61
|
+
createDumper: () => createDumper,
|
|
60
62
|
getMowerCapabilities: () => getMowerCapabilities,
|
|
61
63
|
getVacuumCapabilities: () => getVacuumCapabilities,
|
|
62
64
|
renderMowerSvg: () => renderMowerSvg,
|
|
@@ -67,6 +69,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
67
69
|
|
|
68
70
|
// src/support/version.ts
|
|
69
71
|
var LIBRARY_NAME = "nodedreame";
|
|
72
|
+
var LIBRARY_VERSION = "1.3.0";
|
|
70
73
|
|
|
71
74
|
// src/transport/errors.ts
|
|
72
75
|
var DreameError = class extends Error {
|
|
@@ -2836,11 +2839,11 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
|
|
|
2836
2839
|
}
|
|
2837
2840
|
this.#requireCap(this.#caps.canCleanPerRoom, "cleanZones", "per-room cleaning");
|
|
2838
2841
|
const { repeats, fan, water } = this.#resolveCleanOpts(opts);
|
|
2839
|
-
const areas = zones.map((
|
|
2840
|
-
Math.round(
|
|
2841
|
-
Math.round(
|
|
2842
|
-
Math.round(
|
|
2843
|
-
Math.round(
|
|
2842
|
+
const areas = zones.map((z4) => [
|
|
2843
|
+
Math.round(z4.x0),
|
|
2844
|
+
Math.round(z4.y0),
|
|
2845
|
+
Math.round(z4.x1),
|
|
2846
|
+
Math.round(z4.y1),
|
|
2844
2847
|
repeats,
|
|
2845
2848
|
fan,
|
|
2846
2849
|
water
|
|
@@ -3925,7 +3928,7 @@ var MowerDevice = class _MowerDevice extends BaseDevice {
|
|
|
3925
3928
|
if (zoneIds.length === 0) {
|
|
3926
3929
|
throw new RangeError("startMowingZones: zoneIds must not be empty");
|
|
3927
3930
|
}
|
|
3928
|
-
return this.#sendTask(buildZonePayload(zoneIds.map((
|
|
3931
|
+
return this.#sendTask(buildZonePayload(zoneIds.map((z4) => Math.trunc(z4))));
|
|
3929
3932
|
}
|
|
3930
3933
|
/** Edge / contour mowing (2:50 o:101). Contour ids are two-int pairs [[1,0]]. */
|
|
3931
3934
|
async startMowingEdges(contourIds) {
|
|
@@ -4149,6 +4152,429 @@ var Nodreame = class extends TypedEmitter {
|
|
|
4149
4152
|
await Promise.all(this.#devices.map((d) => d.applySession(session)));
|
|
4150
4153
|
}
|
|
4151
4154
|
};
|
|
4155
|
+
|
|
4156
|
+
// src/diagnostics/redact.ts
|
|
4157
|
+
var REDACTED = "[redacted]";
|
|
4158
|
+
var SENSITIVE_KEY_FRAGMENTS = [
|
|
4159
|
+
// identity / secrets
|
|
4160
|
+
"did",
|
|
4161
|
+
"uid",
|
|
4162
|
+
"token",
|
|
4163
|
+
// accessToken, refreshToken, refresh_token, token_type stripped too (safe)
|
|
4164
|
+
"mac",
|
|
4165
|
+
"serial",
|
|
4166
|
+
"email",
|
|
4167
|
+
"account",
|
|
4168
|
+
"password",
|
|
4169
|
+
"passwd",
|
|
4170
|
+
"secret",
|
|
4171
|
+
"authorization",
|
|
4172
|
+
"auth",
|
|
4173
|
+
"credential",
|
|
4174
|
+
"apikey",
|
|
4175
|
+
"api_key",
|
|
4176
|
+
"clientid",
|
|
4177
|
+
"client_id",
|
|
4178
|
+
// location / PII
|
|
4179
|
+
"gps",
|
|
4180
|
+
"coordinate",
|
|
4181
|
+
"latitude",
|
|
4182
|
+
"longitude",
|
|
4183
|
+
"lat",
|
|
4184
|
+
"lon",
|
|
4185
|
+
"lng",
|
|
4186
|
+
"ssid",
|
|
4187
|
+
"wifi",
|
|
4188
|
+
"bssid",
|
|
4189
|
+
"ipaddr",
|
|
4190
|
+
"localip",
|
|
4191
|
+
"binddomain",
|
|
4192
|
+
"host",
|
|
4193
|
+
"address",
|
|
4194
|
+
"room",
|
|
4195
|
+
"area_name",
|
|
4196
|
+
"areaname",
|
|
4197
|
+
"segmentname",
|
|
4198
|
+
"segment_name",
|
|
4199
|
+
// map binary / geometry (location-revealing)
|
|
4200
|
+
"map_info",
|
|
4201
|
+
"mapinfo",
|
|
4202
|
+
"mapblob",
|
|
4203
|
+
// free-text device names that may carry PII. NOTE: the bare fragment `name` is
|
|
4204
|
+
// intentionally NOT listed — it would over-match the catalog's command `name`
|
|
4205
|
+
// field (an enum-derived, non-sensitive label). The specific custom-name
|
|
4206
|
+
// fields below cover every PII-bearing case.
|
|
4207
|
+
"customname",
|
|
4208
|
+
"devicename",
|
|
4209
|
+
"nickname"
|
|
4210
|
+
];
|
|
4211
|
+
var EXACT_SENSITIVE_KEYS = /* @__PURE__ */ new Set(["ip"]);
|
|
4212
|
+
function isSensitiveKey(key2) {
|
|
4213
|
+
const lower = key2.toLowerCase();
|
|
4214
|
+
if (EXACT_SENSITIVE_KEYS.has(lower)) {
|
|
4215
|
+
return true;
|
|
4216
|
+
}
|
|
4217
|
+
return SENSITIVE_KEY_FRAGMENTS.some((frag) => lower.includes(frag));
|
|
4218
|
+
}
|
|
4219
|
+
function isPlainObject(v) {
|
|
4220
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4221
|
+
}
|
|
4222
|
+
function redact(value) {
|
|
4223
|
+
if (Array.isArray(value)) {
|
|
4224
|
+
return value.map((v) => redact(v));
|
|
4225
|
+
}
|
|
4226
|
+
if (isPlainObject(value)) {
|
|
4227
|
+
const out = {};
|
|
4228
|
+
for (const [k, v] of Object.entries(value)) {
|
|
4229
|
+
out[k] = isSensitiveKey(k) ? REDACTED : redact(v);
|
|
4230
|
+
}
|
|
4231
|
+
return out;
|
|
4232
|
+
}
|
|
4233
|
+
return value;
|
|
4234
|
+
}
|
|
4235
|
+
|
|
4236
|
+
// src/diagnostics/dump-format.ts
|
|
4237
|
+
var import_zod3 = require("zod");
|
|
4238
|
+
var DumpScalarSchema = import_zod3.z.union([import_zod3.z.string(), import_zod3.z.number(), import_zod3.z.boolean()]);
|
|
4239
|
+
var PropertyObservationSchema = import_zod3.z.object({
|
|
4240
|
+
values: import_zod3.z.array(DumpScalarSchema),
|
|
4241
|
+
unmapped: import_zod3.z.array(DumpScalarSchema),
|
|
4242
|
+
enum: import_zod3.z.string().optional(),
|
|
4243
|
+
count: import_zod3.z.number(),
|
|
4244
|
+
firstSeen: import_zod3.z.number(),
|
|
4245
|
+
lastSeen: import_zod3.z.number()
|
|
4246
|
+
});
|
|
4247
|
+
var EventObservationSchema = import_zod3.z.object({
|
|
4248
|
+
at: import_zod3.z.number(),
|
|
4249
|
+
type: import_zod3.z.string(),
|
|
4250
|
+
data: import_zod3.z.unknown().optional()
|
|
4251
|
+
});
|
|
4252
|
+
var RawFrameSchema = import_zod3.z.object({
|
|
4253
|
+
at: import_zod3.z.number(),
|
|
4254
|
+
source: import_zod3.z.string(),
|
|
4255
|
+
payload: import_zod3.z.unknown()
|
|
4256
|
+
});
|
|
4257
|
+
var CommandSchema = import_zod3.z.object({
|
|
4258
|
+
name: import_zod3.z.string(),
|
|
4259
|
+
siid: import_zod3.z.number().optional(),
|
|
4260
|
+
aiid: import_zod3.z.number().optional()
|
|
4261
|
+
});
|
|
4262
|
+
var SensorSchema = import_zod3.z.object({
|
|
4263
|
+
model: import_zod3.z.string(),
|
|
4264
|
+
channel: import_zod3.z.number().optional()
|
|
4265
|
+
});
|
|
4266
|
+
var DeviceDumpSchema = import_zod3.z.object({
|
|
4267
|
+
schemaVersion: import_zod3.z.literal(1),
|
|
4268
|
+
library: import_zod3.z.union([import_zod3.z.literal("nodedreame"), import_zod3.z.literal("nodewitt")]),
|
|
4269
|
+
libraryVersion: import_zod3.z.string(),
|
|
4270
|
+
device: import_zod3.z.object({
|
|
4271
|
+
model: import_zod3.z.string(),
|
|
4272
|
+
firmware: import_zod3.z.string().optional(),
|
|
4273
|
+
region: import_zod3.z.string().optional(),
|
|
4274
|
+
type: import_zod3.z.string().optional()
|
|
4275
|
+
}),
|
|
4276
|
+
observations: import_zod3.z.object({
|
|
4277
|
+
properties: import_zod3.z.record(import_zod3.z.string(), PropertyObservationSchema),
|
|
4278
|
+
events: import_zod3.z.array(EventObservationSchema),
|
|
4279
|
+
rawFrames: import_zod3.z.array(RawFrameSchema).optional()
|
|
4280
|
+
}),
|
|
4281
|
+
catalog: import_zod3.z.object({
|
|
4282
|
+
commands: import_zod3.z.array(CommandSchema).optional(),
|
|
4283
|
+
capabilities: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.unknown()).optional(),
|
|
4284
|
+
sensors: import_zod3.z.array(SensorSchema).optional()
|
|
4285
|
+
}),
|
|
4286
|
+
meta: import_zod3.z.object({
|
|
4287
|
+
startedAt: import_zod3.z.number(),
|
|
4288
|
+
durationMs: import_zod3.z.number(),
|
|
4289
|
+
generatedAt: import_zod3.z.number()
|
|
4290
|
+
})
|
|
4291
|
+
});
|
|
4292
|
+
|
|
4293
|
+
// src/diagnostics/dumper.ts
|
|
4294
|
+
function commandsForFamily(family) {
|
|
4295
|
+
const commands = [];
|
|
4296
|
+
if (family === "vacuum") {
|
|
4297
|
+
for (const [name, ref] of Object.entries(VACUUM_ACTION)) {
|
|
4298
|
+
commands.push({ name, siid: ref.siid, aiid: ref.aiid });
|
|
4299
|
+
}
|
|
4300
|
+
} else if (family === "mower") {
|
|
4301
|
+
for (const [name, ref] of Object.entries(MOWER_ACTION)) {
|
|
4302
|
+
commands.push({ name, siid: ref.siid, aiid: ref.aiid });
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
return commands;
|
|
4306
|
+
}
|
|
4307
|
+
function decoderFor(enumName, members) {
|
|
4308
|
+
const lookup = enumLookup(members);
|
|
4309
|
+
return { enumName, isMember: (raw) => lookup(raw) !== null };
|
|
4310
|
+
}
|
|
4311
|
+
function vacuumDecoders() {
|
|
4312
|
+
return {
|
|
4313
|
+
"2.1": decoderFor(
|
|
4314
|
+
"MiotState",
|
|
4315
|
+
Object.values(MiotState).filter((v) => typeof v === "number")
|
|
4316
|
+
),
|
|
4317
|
+
"3.2": decoderFor(
|
|
4318
|
+
"ChargingStatus",
|
|
4319
|
+
Object.values(ChargingStatus).filter((v) => typeof v === "number")
|
|
4320
|
+
),
|
|
4321
|
+
"4.4": decoderFor(
|
|
4322
|
+
"SuctionLevel",
|
|
4323
|
+
Object.values(SuctionLevel).filter((v) => typeof v === "number")
|
|
4324
|
+
),
|
|
4325
|
+
"4.5": decoderFor(
|
|
4326
|
+
"WaterVolume",
|
|
4327
|
+
Object.values(WaterVolume).filter((v) => typeof v === "number")
|
|
4328
|
+
),
|
|
4329
|
+
"2.6": decoderFor(
|
|
4330
|
+
"CleaningMode",
|
|
4331
|
+
Object.values(CleaningMode).filter((v) => typeof v === "number")
|
|
4332
|
+
),
|
|
4333
|
+
"4.1": decoderFor(
|
|
4334
|
+
"TaskStatus",
|
|
4335
|
+
Object.values(TaskStatus).filter((v) => typeof v === "number")
|
|
4336
|
+
)
|
|
4337
|
+
};
|
|
4338
|
+
}
|
|
4339
|
+
function mowerDecoders() {
|
|
4340
|
+
return {
|
|
4341
|
+
"2.1": decoderFor(
|
|
4342
|
+
"MowerStatus",
|
|
4343
|
+
Object.values(MowerStatus).filter((v) => typeof v === "number")
|
|
4344
|
+
),
|
|
4345
|
+
"3.2": decoderFor(
|
|
4346
|
+
"MowerChargingStatus",
|
|
4347
|
+
Object.values(MowerChargingStatus).filter(
|
|
4348
|
+
(v) => typeof v === "number"
|
|
4349
|
+
)
|
|
4350
|
+
),
|
|
4351
|
+
"5.104": decoderFor(
|
|
4352
|
+
"MowerTaskStatus",
|
|
4353
|
+
Object.values(MowerTaskStatus).filter((v) => typeof v === "number")
|
|
4354
|
+
),
|
|
4355
|
+
"2.2": decoderFor(
|
|
4356
|
+
"MowerFault",
|
|
4357
|
+
Object.values(MowerFault).filter((v) => typeof v === "number")
|
|
4358
|
+
)
|
|
4359
|
+
};
|
|
4360
|
+
}
|
|
4361
|
+
function toScalar(value) {
|
|
4362
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
4363
|
+
return value;
|
|
4364
|
+
}
|
|
4365
|
+
return null;
|
|
4366
|
+
}
|
|
4367
|
+
var PropertyAccumulator = class {
|
|
4368
|
+
#decoders;
|
|
4369
|
+
#entries = /* @__PURE__ */ new Map();
|
|
4370
|
+
constructor(decoders = {}) {
|
|
4371
|
+
this.#decoders = decoders;
|
|
4372
|
+
}
|
|
4373
|
+
record(siid, piid, rawValue, at) {
|
|
4374
|
+
const key2 = `${siid}.${piid}`;
|
|
4375
|
+
const scalar = toScalar(rawValue);
|
|
4376
|
+
let entry = this.#entries.get(key2);
|
|
4377
|
+
if (!entry) {
|
|
4378
|
+
entry = { values: [], unmapped: [], count: 0, firstSeen: at, lastSeen: at };
|
|
4379
|
+
this.#entries.set(key2, entry);
|
|
4380
|
+
}
|
|
4381
|
+
entry.count += 1;
|
|
4382
|
+
entry.lastSeen = at;
|
|
4383
|
+
if (scalar === null) {
|
|
4384
|
+
return;
|
|
4385
|
+
}
|
|
4386
|
+
if (!entry.values.includes(scalar)) {
|
|
4387
|
+
entry.values.push(scalar);
|
|
4388
|
+
}
|
|
4389
|
+
const decoder = this.#decoders[key2];
|
|
4390
|
+
if (decoder && typeof scalar === "number" && !decoder.isMember(scalar)) {
|
|
4391
|
+
if (!entry.unmapped.includes(scalar)) {
|
|
4392
|
+
entry.unmapped.push(scalar);
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
snapshot() {
|
|
4397
|
+
const out = {};
|
|
4398
|
+
for (const [key2, e] of this.#entries) {
|
|
4399
|
+
const decoder = this.#decoders[key2];
|
|
4400
|
+
const base = {
|
|
4401
|
+
values: [...e.values],
|
|
4402
|
+
unmapped: [...e.unmapped],
|
|
4403
|
+
count: e.count,
|
|
4404
|
+
firstSeen: e.firstSeen,
|
|
4405
|
+
lastSeen: e.lastSeen
|
|
4406
|
+
};
|
|
4407
|
+
out[key2] = decoder ? { ...base, enum: decoder.enumName } : base;
|
|
4408
|
+
}
|
|
4409
|
+
return out;
|
|
4410
|
+
}
|
|
4411
|
+
};
|
|
4412
|
+
var DEFAULT_REFRESH_INTERVAL_MS = 3e4;
|
|
4413
|
+
var DEFAULT_MAX_RAW_FRAMES = 500;
|
|
4414
|
+
var Dumper = class {
|
|
4415
|
+
#device;
|
|
4416
|
+
#opts;
|
|
4417
|
+
#acc;
|
|
4418
|
+
#events = [];
|
|
4419
|
+
#rawFrames = [];
|
|
4420
|
+
#catalog;
|
|
4421
|
+
#type;
|
|
4422
|
+
#onProperty;
|
|
4423
|
+
#onEvent;
|
|
4424
|
+
#timer = null;
|
|
4425
|
+
#startedAt = 0;
|
|
4426
|
+
#started = false;
|
|
4427
|
+
constructor(device, catalog, type, decoders, options = {}) {
|
|
4428
|
+
this.#device = device;
|
|
4429
|
+
this.#catalog = catalog;
|
|
4430
|
+
this.#type = type;
|
|
4431
|
+
this.#acc = new PropertyAccumulator(decoders);
|
|
4432
|
+
this.#opts = {
|
|
4433
|
+
captureRawFrames: options.captureRawFrames ?? false,
|
|
4434
|
+
maxRawFrames: options.maxRawFrames ?? DEFAULT_MAX_RAW_FRAMES,
|
|
4435
|
+
refreshIntervalMs: options.refreshIntervalMs ?? DEFAULT_REFRESH_INTERVAL_MS
|
|
4436
|
+
};
|
|
4437
|
+
this.#onProperty = (e) => {
|
|
4438
|
+
this.#acc.record(e.siid, e.piid, e.value, Date.now());
|
|
4439
|
+
this.#pushRaw("mqtt:property", e);
|
|
4440
|
+
};
|
|
4441
|
+
this.#onEvent = (e) => {
|
|
4442
|
+
this.#events.push({
|
|
4443
|
+
at: Date.now(),
|
|
4444
|
+
type: `${e.siid}.${e.eiid}`,
|
|
4445
|
+
data: { arguments: e.arguments }
|
|
4446
|
+
});
|
|
4447
|
+
this.#pushRaw("mqtt:event", e);
|
|
4448
|
+
};
|
|
4449
|
+
}
|
|
4450
|
+
/** Attach to the live stream + arm the periodic shadow refresh. Idempotent. */
|
|
4451
|
+
async start() {
|
|
4452
|
+
if (this.#started) {
|
|
4453
|
+
return;
|
|
4454
|
+
}
|
|
4455
|
+
this.#started = true;
|
|
4456
|
+
this.#startedAt = Date.now();
|
|
4457
|
+
this.#device.on("propertyChanged", this.#onProperty);
|
|
4458
|
+
this.#device.on("event", this.#onEvent);
|
|
4459
|
+
await this.#refresh();
|
|
4460
|
+
if (this.#opts.refreshIntervalMs > 0 && this.#device.refreshFromCache) {
|
|
4461
|
+
this.#timer = setInterval(() => void this.#refresh(), this.#opts.refreshIntervalMs);
|
|
4462
|
+
this.#timer.unref?.();
|
|
4463
|
+
}
|
|
4464
|
+
}
|
|
4465
|
+
/** Detach every listener + clear the timer. Idempotent. */
|
|
4466
|
+
stop() {
|
|
4467
|
+
if (!this.#started) {
|
|
4468
|
+
return Promise.resolve();
|
|
4469
|
+
}
|
|
4470
|
+
this.#started = false;
|
|
4471
|
+
if (this.#timer) {
|
|
4472
|
+
clearInterval(this.#timer);
|
|
4473
|
+
this.#timer = null;
|
|
4474
|
+
}
|
|
4475
|
+
this.#device.off("propertyChanged", this.#onProperty);
|
|
4476
|
+
this.#device.off("event", this.#onEvent);
|
|
4477
|
+
return Promise.resolve();
|
|
4478
|
+
}
|
|
4479
|
+
/** Build the anonymized, schema-valid dump. Works live or after {@link stop}. */
|
|
4480
|
+
export() {
|
|
4481
|
+
const now = Date.now();
|
|
4482
|
+
const dump = {
|
|
4483
|
+
schemaVersion: 1,
|
|
4484
|
+
library: "nodedreame",
|
|
4485
|
+
libraryVersion: LIBRARY_VERSION,
|
|
4486
|
+
device: {
|
|
4487
|
+
model: this.#device.model,
|
|
4488
|
+
type: this.#type
|
|
4489
|
+
},
|
|
4490
|
+
observations: {
|
|
4491
|
+
properties: this.#acc.snapshot(),
|
|
4492
|
+
events: [...this.#events],
|
|
4493
|
+
...this.#opts.captureRawFrames ? { rawFrames: this.#redactFrames() } : {}
|
|
4494
|
+
},
|
|
4495
|
+
catalog: this.#catalog,
|
|
4496
|
+
meta: {
|
|
4497
|
+
startedAt: this.#startedAt,
|
|
4498
|
+
durationMs: Math.max(0, now - this.#startedAt),
|
|
4499
|
+
generatedAt: now
|
|
4500
|
+
}
|
|
4501
|
+
};
|
|
4502
|
+
const anonymized = redact(dump);
|
|
4503
|
+
return DeviceDumpSchema.parse(anonymized);
|
|
4504
|
+
}
|
|
4505
|
+
/** Deterministic pretty JSON of {@link export}. */
|
|
4506
|
+
exportJson() {
|
|
4507
|
+
return JSON.stringify(this.export(), null, 2);
|
|
4508
|
+
}
|
|
4509
|
+
async #refresh() {
|
|
4510
|
+
if (this.#device.refreshFromCache) {
|
|
4511
|
+
try {
|
|
4512
|
+
await this.#device.refreshFromCache();
|
|
4513
|
+
} catch {
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4517
|
+
#pushRaw(source, payload) {
|
|
4518
|
+
if (!this.#opts.captureRawFrames) {
|
|
4519
|
+
return;
|
|
4520
|
+
}
|
|
4521
|
+
this.#rawFrames.push({ at: Date.now(), source, payload });
|
|
4522
|
+
if (this.#rawFrames.length > this.#opts.maxRawFrames) {
|
|
4523
|
+
this.#rawFrames.shift();
|
|
4524
|
+
}
|
|
4525
|
+
}
|
|
4526
|
+
#redactFrames() {
|
|
4527
|
+
return this.#rawFrames.map((f) => ({ at: f.at, source: f.source, payload: redact(f.payload) }));
|
|
4528
|
+
}
|
|
4529
|
+
};
|
|
4530
|
+
function familyOf(device) {
|
|
4531
|
+
if (device instanceof VacuumDevice) {
|
|
4532
|
+
return "vacuum";
|
|
4533
|
+
}
|
|
4534
|
+
if (device instanceof MowerDevice) {
|
|
4535
|
+
return "mower";
|
|
4536
|
+
}
|
|
4537
|
+
if (!(device instanceof BaseDevice)) {
|
|
4538
|
+
if (device.model.startsWith("dreame.vacuum.")) {
|
|
4539
|
+
return "vacuum";
|
|
4540
|
+
}
|
|
4541
|
+
if (device.model.startsWith("dreame.mower.")) {
|
|
4542
|
+
return "mower";
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
return "device";
|
|
4546
|
+
}
|
|
4547
|
+
function decodersFor(device) {
|
|
4548
|
+
switch (familyOf(device)) {
|
|
4549
|
+
case "vacuum":
|
|
4550
|
+
return vacuumDecoders();
|
|
4551
|
+
case "mower":
|
|
4552
|
+
return mowerDecoders();
|
|
4553
|
+
default:
|
|
4554
|
+
return {};
|
|
4555
|
+
}
|
|
4556
|
+
}
|
|
4557
|
+
function typeFor(device) {
|
|
4558
|
+
return familyOf(device);
|
|
4559
|
+
}
|
|
4560
|
+
function catalogFor(device) {
|
|
4561
|
+
return {
|
|
4562
|
+
commands: commandsForFamily(familyOf(device)),
|
|
4563
|
+
capabilities: { tokens: [...device.capabilities.list()] }
|
|
4564
|
+
};
|
|
4565
|
+
}
|
|
4566
|
+
function createDumper(target, options) {
|
|
4567
|
+
return new Dumper(
|
|
4568
|
+
target,
|
|
4569
|
+
catalogFor(target),
|
|
4570
|
+
typeFor(target),
|
|
4571
|
+
decodersFor(target),
|
|
4572
|
+
options ?? {}
|
|
4573
|
+
);
|
|
4574
|
+
}
|
|
4575
|
+
function createClientDumper(client, options) {
|
|
4576
|
+
return client.devices.map((d) => createDumper(d, options));
|
|
4577
|
+
}
|
|
4152
4578
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4153
4579
|
0 && (module.exports = {
|
|
4154
4580
|
BaseDevice,
|
|
@@ -4178,6 +4604,8 @@ var Nodreame = class extends TypedEmitter {
|
|
|
4178
4604
|
VacuumCapabilityResolver,
|
|
4179
4605
|
VacuumDevice,
|
|
4180
4606
|
WaterVolume,
|
|
4607
|
+
createClientDumper,
|
|
4608
|
+
createDumper,
|
|
4181
4609
|
getMowerCapabilities,
|
|
4182
4610
|
getVacuumCapabilities,
|
|
4183
4611
|
renderMowerSvg,
|