@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/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/support/version.ts
2
2
  var LIBRARY_NAME = "nodedreame";
3
+ var LIBRARY_VERSION = "1.3.0";
3
4
 
4
5
  // src/transport/errors.ts
5
6
  var DreameError = class extends Error {
@@ -2769,11 +2770,11 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
2769
2770
  }
2770
2771
  this.#requireCap(this.#caps.canCleanPerRoom, "cleanZones", "per-room cleaning");
2771
2772
  const { repeats, fan, water } = this.#resolveCleanOpts(opts);
2772
- const areas = zones.map((z3) => [
2773
- Math.round(z3.x0),
2774
- Math.round(z3.y0),
2775
- Math.round(z3.x1),
2776
- Math.round(z3.y1),
2773
+ const areas = zones.map((z4) => [
2774
+ Math.round(z4.x0),
2775
+ Math.round(z4.y0),
2776
+ Math.round(z4.x1),
2777
+ Math.round(z4.y1),
2777
2778
  repeats,
2778
2779
  fan,
2779
2780
  water
@@ -3858,7 +3859,7 @@ var MowerDevice = class _MowerDevice extends BaseDevice {
3858
3859
  if (zoneIds.length === 0) {
3859
3860
  throw new RangeError("startMowingZones: zoneIds must not be empty");
3860
3861
  }
3861
- return this.#sendTask(buildZonePayload(zoneIds.map((z3) => Math.trunc(z3))));
3862
+ return this.#sendTask(buildZonePayload(zoneIds.map((z4) => Math.trunc(z4))));
3862
3863
  }
3863
3864
  /** Edge / contour mowing (2:50 o:101). Contour ids are two-int pairs [[1,0]]. */
3864
3865
  async startMowingEdges(contourIds) {
@@ -4082,6 +4083,429 @@ var Nodreame = class extends TypedEmitter {
4082
4083
  await Promise.all(this.#devices.map((d) => d.applySession(session)));
4083
4084
  }
4084
4085
  };
4086
+
4087
+ // src/diagnostics/redact.ts
4088
+ var REDACTED = "[redacted]";
4089
+ var SENSITIVE_KEY_FRAGMENTS = [
4090
+ // identity / secrets
4091
+ "did",
4092
+ "uid",
4093
+ "token",
4094
+ // accessToken, refreshToken, refresh_token, token_type stripped too (safe)
4095
+ "mac",
4096
+ "serial",
4097
+ "email",
4098
+ "account",
4099
+ "password",
4100
+ "passwd",
4101
+ "secret",
4102
+ "authorization",
4103
+ "auth",
4104
+ "credential",
4105
+ "apikey",
4106
+ "api_key",
4107
+ "clientid",
4108
+ "client_id",
4109
+ // location / PII
4110
+ "gps",
4111
+ "coordinate",
4112
+ "latitude",
4113
+ "longitude",
4114
+ "lat",
4115
+ "lon",
4116
+ "lng",
4117
+ "ssid",
4118
+ "wifi",
4119
+ "bssid",
4120
+ "ipaddr",
4121
+ "localip",
4122
+ "binddomain",
4123
+ "host",
4124
+ "address",
4125
+ "room",
4126
+ "area_name",
4127
+ "areaname",
4128
+ "segmentname",
4129
+ "segment_name",
4130
+ // map binary / geometry (location-revealing)
4131
+ "map_info",
4132
+ "mapinfo",
4133
+ "mapblob",
4134
+ // free-text device names that may carry PII. NOTE: the bare fragment `name` is
4135
+ // intentionally NOT listed — it would over-match the catalog's command `name`
4136
+ // field (an enum-derived, non-sensitive label). The specific custom-name
4137
+ // fields below cover every PII-bearing case.
4138
+ "customname",
4139
+ "devicename",
4140
+ "nickname"
4141
+ ];
4142
+ var EXACT_SENSITIVE_KEYS = /* @__PURE__ */ new Set(["ip"]);
4143
+ function isSensitiveKey(key2) {
4144
+ const lower = key2.toLowerCase();
4145
+ if (EXACT_SENSITIVE_KEYS.has(lower)) {
4146
+ return true;
4147
+ }
4148
+ return SENSITIVE_KEY_FRAGMENTS.some((frag) => lower.includes(frag));
4149
+ }
4150
+ function isPlainObject(v) {
4151
+ return typeof v === "object" && v !== null && !Array.isArray(v);
4152
+ }
4153
+ function redact(value) {
4154
+ if (Array.isArray(value)) {
4155
+ return value.map((v) => redact(v));
4156
+ }
4157
+ if (isPlainObject(value)) {
4158
+ const out = {};
4159
+ for (const [k, v] of Object.entries(value)) {
4160
+ out[k] = isSensitiveKey(k) ? REDACTED : redact(v);
4161
+ }
4162
+ return out;
4163
+ }
4164
+ return value;
4165
+ }
4166
+
4167
+ // src/diagnostics/dump-format.ts
4168
+ import { z as z3 } from "zod";
4169
+ var DumpScalarSchema = z3.union([z3.string(), z3.number(), z3.boolean()]);
4170
+ var PropertyObservationSchema = z3.object({
4171
+ values: z3.array(DumpScalarSchema),
4172
+ unmapped: z3.array(DumpScalarSchema),
4173
+ enum: z3.string().optional(),
4174
+ count: z3.number(),
4175
+ firstSeen: z3.number(),
4176
+ lastSeen: z3.number()
4177
+ });
4178
+ var EventObservationSchema = z3.object({
4179
+ at: z3.number(),
4180
+ type: z3.string(),
4181
+ data: z3.unknown().optional()
4182
+ });
4183
+ var RawFrameSchema = z3.object({
4184
+ at: z3.number(),
4185
+ source: z3.string(),
4186
+ payload: z3.unknown()
4187
+ });
4188
+ var CommandSchema = z3.object({
4189
+ name: z3.string(),
4190
+ siid: z3.number().optional(),
4191
+ aiid: z3.number().optional()
4192
+ });
4193
+ var SensorSchema = z3.object({
4194
+ model: z3.string(),
4195
+ channel: z3.number().optional()
4196
+ });
4197
+ var DeviceDumpSchema = z3.object({
4198
+ schemaVersion: z3.literal(1),
4199
+ library: z3.union([z3.literal("nodedreame"), z3.literal("nodewitt")]),
4200
+ libraryVersion: z3.string(),
4201
+ device: z3.object({
4202
+ model: z3.string(),
4203
+ firmware: z3.string().optional(),
4204
+ region: z3.string().optional(),
4205
+ type: z3.string().optional()
4206
+ }),
4207
+ observations: z3.object({
4208
+ properties: z3.record(z3.string(), PropertyObservationSchema),
4209
+ events: z3.array(EventObservationSchema),
4210
+ rawFrames: z3.array(RawFrameSchema).optional()
4211
+ }),
4212
+ catalog: z3.object({
4213
+ commands: z3.array(CommandSchema).optional(),
4214
+ capabilities: z3.record(z3.string(), z3.unknown()).optional(),
4215
+ sensors: z3.array(SensorSchema).optional()
4216
+ }),
4217
+ meta: z3.object({
4218
+ startedAt: z3.number(),
4219
+ durationMs: z3.number(),
4220
+ generatedAt: z3.number()
4221
+ })
4222
+ });
4223
+
4224
+ // src/diagnostics/dumper.ts
4225
+ function commandsForFamily(family) {
4226
+ const commands = [];
4227
+ if (family === "vacuum") {
4228
+ for (const [name, ref] of Object.entries(VACUUM_ACTION)) {
4229
+ commands.push({ name, siid: ref.siid, aiid: ref.aiid });
4230
+ }
4231
+ } else if (family === "mower") {
4232
+ for (const [name, ref] of Object.entries(MOWER_ACTION)) {
4233
+ commands.push({ name, siid: ref.siid, aiid: ref.aiid });
4234
+ }
4235
+ }
4236
+ return commands;
4237
+ }
4238
+ function decoderFor(enumName, members) {
4239
+ const lookup = enumLookup(members);
4240
+ return { enumName, isMember: (raw) => lookup(raw) !== null };
4241
+ }
4242
+ function vacuumDecoders() {
4243
+ return {
4244
+ "2.1": decoderFor(
4245
+ "MiotState",
4246
+ Object.values(MiotState).filter((v) => typeof v === "number")
4247
+ ),
4248
+ "3.2": decoderFor(
4249
+ "ChargingStatus",
4250
+ Object.values(ChargingStatus).filter((v) => typeof v === "number")
4251
+ ),
4252
+ "4.4": decoderFor(
4253
+ "SuctionLevel",
4254
+ Object.values(SuctionLevel).filter((v) => typeof v === "number")
4255
+ ),
4256
+ "4.5": decoderFor(
4257
+ "WaterVolume",
4258
+ Object.values(WaterVolume).filter((v) => typeof v === "number")
4259
+ ),
4260
+ "2.6": decoderFor(
4261
+ "CleaningMode",
4262
+ Object.values(CleaningMode).filter((v) => typeof v === "number")
4263
+ ),
4264
+ "4.1": decoderFor(
4265
+ "TaskStatus",
4266
+ Object.values(TaskStatus).filter((v) => typeof v === "number")
4267
+ )
4268
+ };
4269
+ }
4270
+ function mowerDecoders() {
4271
+ return {
4272
+ "2.1": decoderFor(
4273
+ "MowerStatus",
4274
+ Object.values(MowerStatus).filter((v) => typeof v === "number")
4275
+ ),
4276
+ "3.2": decoderFor(
4277
+ "MowerChargingStatus",
4278
+ Object.values(MowerChargingStatus).filter(
4279
+ (v) => typeof v === "number"
4280
+ )
4281
+ ),
4282
+ "5.104": decoderFor(
4283
+ "MowerTaskStatus",
4284
+ Object.values(MowerTaskStatus).filter((v) => typeof v === "number")
4285
+ ),
4286
+ "2.2": decoderFor(
4287
+ "MowerFault",
4288
+ Object.values(MowerFault).filter((v) => typeof v === "number")
4289
+ )
4290
+ };
4291
+ }
4292
+ function toScalar(value) {
4293
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
4294
+ return value;
4295
+ }
4296
+ return null;
4297
+ }
4298
+ var PropertyAccumulator = class {
4299
+ #decoders;
4300
+ #entries = /* @__PURE__ */ new Map();
4301
+ constructor(decoders = {}) {
4302
+ this.#decoders = decoders;
4303
+ }
4304
+ record(siid, piid, rawValue, at) {
4305
+ const key2 = `${siid}.${piid}`;
4306
+ const scalar = toScalar(rawValue);
4307
+ let entry = this.#entries.get(key2);
4308
+ if (!entry) {
4309
+ entry = { values: [], unmapped: [], count: 0, firstSeen: at, lastSeen: at };
4310
+ this.#entries.set(key2, entry);
4311
+ }
4312
+ entry.count += 1;
4313
+ entry.lastSeen = at;
4314
+ if (scalar === null) {
4315
+ return;
4316
+ }
4317
+ if (!entry.values.includes(scalar)) {
4318
+ entry.values.push(scalar);
4319
+ }
4320
+ const decoder = this.#decoders[key2];
4321
+ if (decoder && typeof scalar === "number" && !decoder.isMember(scalar)) {
4322
+ if (!entry.unmapped.includes(scalar)) {
4323
+ entry.unmapped.push(scalar);
4324
+ }
4325
+ }
4326
+ }
4327
+ snapshot() {
4328
+ const out = {};
4329
+ for (const [key2, e] of this.#entries) {
4330
+ const decoder = this.#decoders[key2];
4331
+ const base = {
4332
+ values: [...e.values],
4333
+ unmapped: [...e.unmapped],
4334
+ count: e.count,
4335
+ firstSeen: e.firstSeen,
4336
+ lastSeen: e.lastSeen
4337
+ };
4338
+ out[key2] = decoder ? { ...base, enum: decoder.enumName } : base;
4339
+ }
4340
+ return out;
4341
+ }
4342
+ };
4343
+ var DEFAULT_REFRESH_INTERVAL_MS = 3e4;
4344
+ var DEFAULT_MAX_RAW_FRAMES = 500;
4345
+ var Dumper = class {
4346
+ #device;
4347
+ #opts;
4348
+ #acc;
4349
+ #events = [];
4350
+ #rawFrames = [];
4351
+ #catalog;
4352
+ #type;
4353
+ #onProperty;
4354
+ #onEvent;
4355
+ #timer = null;
4356
+ #startedAt = 0;
4357
+ #started = false;
4358
+ constructor(device, catalog, type, decoders, options = {}) {
4359
+ this.#device = device;
4360
+ this.#catalog = catalog;
4361
+ this.#type = type;
4362
+ this.#acc = new PropertyAccumulator(decoders);
4363
+ this.#opts = {
4364
+ captureRawFrames: options.captureRawFrames ?? false,
4365
+ maxRawFrames: options.maxRawFrames ?? DEFAULT_MAX_RAW_FRAMES,
4366
+ refreshIntervalMs: options.refreshIntervalMs ?? DEFAULT_REFRESH_INTERVAL_MS
4367
+ };
4368
+ this.#onProperty = (e) => {
4369
+ this.#acc.record(e.siid, e.piid, e.value, Date.now());
4370
+ this.#pushRaw("mqtt:property", e);
4371
+ };
4372
+ this.#onEvent = (e) => {
4373
+ this.#events.push({
4374
+ at: Date.now(),
4375
+ type: `${e.siid}.${e.eiid}`,
4376
+ data: { arguments: e.arguments }
4377
+ });
4378
+ this.#pushRaw("mqtt:event", e);
4379
+ };
4380
+ }
4381
+ /** Attach to the live stream + arm the periodic shadow refresh. Idempotent. */
4382
+ async start() {
4383
+ if (this.#started) {
4384
+ return;
4385
+ }
4386
+ this.#started = true;
4387
+ this.#startedAt = Date.now();
4388
+ this.#device.on("propertyChanged", this.#onProperty);
4389
+ this.#device.on("event", this.#onEvent);
4390
+ await this.#refresh();
4391
+ if (this.#opts.refreshIntervalMs > 0 && this.#device.refreshFromCache) {
4392
+ this.#timer = setInterval(() => void this.#refresh(), this.#opts.refreshIntervalMs);
4393
+ this.#timer.unref?.();
4394
+ }
4395
+ }
4396
+ /** Detach every listener + clear the timer. Idempotent. */
4397
+ stop() {
4398
+ if (!this.#started) {
4399
+ return Promise.resolve();
4400
+ }
4401
+ this.#started = false;
4402
+ if (this.#timer) {
4403
+ clearInterval(this.#timer);
4404
+ this.#timer = null;
4405
+ }
4406
+ this.#device.off("propertyChanged", this.#onProperty);
4407
+ this.#device.off("event", this.#onEvent);
4408
+ return Promise.resolve();
4409
+ }
4410
+ /** Build the anonymized, schema-valid dump. Works live or after {@link stop}. */
4411
+ export() {
4412
+ const now = Date.now();
4413
+ const dump = {
4414
+ schemaVersion: 1,
4415
+ library: "nodedreame",
4416
+ libraryVersion: LIBRARY_VERSION,
4417
+ device: {
4418
+ model: this.#device.model,
4419
+ type: this.#type
4420
+ },
4421
+ observations: {
4422
+ properties: this.#acc.snapshot(),
4423
+ events: [...this.#events],
4424
+ ...this.#opts.captureRawFrames ? { rawFrames: this.#redactFrames() } : {}
4425
+ },
4426
+ catalog: this.#catalog,
4427
+ meta: {
4428
+ startedAt: this.#startedAt,
4429
+ durationMs: Math.max(0, now - this.#startedAt),
4430
+ generatedAt: now
4431
+ }
4432
+ };
4433
+ const anonymized = redact(dump);
4434
+ return DeviceDumpSchema.parse(anonymized);
4435
+ }
4436
+ /** Deterministic pretty JSON of {@link export}. */
4437
+ exportJson() {
4438
+ return JSON.stringify(this.export(), null, 2);
4439
+ }
4440
+ async #refresh() {
4441
+ if (this.#device.refreshFromCache) {
4442
+ try {
4443
+ await this.#device.refreshFromCache();
4444
+ } catch {
4445
+ }
4446
+ }
4447
+ }
4448
+ #pushRaw(source, payload) {
4449
+ if (!this.#opts.captureRawFrames) {
4450
+ return;
4451
+ }
4452
+ this.#rawFrames.push({ at: Date.now(), source, payload });
4453
+ if (this.#rawFrames.length > this.#opts.maxRawFrames) {
4454
+ this.#rawFrames.shift();
4455
+ }
4456
+ }
4457
+ #redactFrames() {
4458
+ return this.#rawFrames.map((f) => ({ at: f.at, source: f.source, payload: redact(f.payload) }));
4459
+ }
4460
+ };
4461
+ function familyOf(device) {
4462
+ if (device instanceof VacuumDevice) {
4463
+ return "vacuum";
4464
+ }
4465
+ if (device instanceof MowerDevice) {
4466
+ return "mower";
4467
+ }
4468
+ if (!(device instanceof BaseDevice)) {
4469
+ if (device.model.startsWith("dreame.vacuum.")) {
4470
+ return "vacuum";
4471
+ }
4472
+ if (device.model.startsWith("dreame.mower.")) {
4473
+ return "mower";
4474
+ }
4475
+ }
4476
+ return "device";
4477
+ }
4478
+ function decodersFor(device) {
4479
+ switch (familyOf(device)) {
4480
+ case "vacuum":
4481
+ return vacuumDecoders();
4482
+ case "mower":
4483
+ return mowerDecoders();
4484
+ default:
4485
+ return {};
4486
+ }
4487
+ }
4488
+ function typeFor(device) {
4489
+ return familyOf(device);
4490
+ }
4491
+ function catalogFor(device) {
4492
+ return {
4493
+ commands: commandsForFamily(familyOf(device)),
4494
+ capabilities: { tokens: [...device.capabilities.list()] }
4495
+ };
4496
+ }
4497
+ function createDumper(target, options) {
4498
+ return new Dumper(
4499
+ target,
4500
+ catalogFor(target),
4501
+ typeFor(target),
4502
+ decodersFor(target),
4503
+ options ?? {}
4504
+ );
4505
+ }
4506
+ function createClientDumper(client, options) {
4507
+ return client.devices.map((d) => createDumper(d, options));
4508
+ }
4085
4509
  export {
4086
4510
  BaseDevice,
4087
4511
  ChargingStatus,
@@ -4110,6 +4534,8 @@ export {
4110
4534
  VacuumCapabilityResolver,
4111
4535
  VacuumDevice,
4112
4536
  WaterVolume,
4537
+ createClientDumper,
4538
+ createDumper,
4113
4539
  getMowerCapabilities,
4114
4540
  getVacuumCapabilities,
4115
4541
  renderMowerSvg,