@apocaliss92/nodedreame 1.11.5 → 1.11.9

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.cjs CHANGED
@@ -75,8 +75,10 @@ __export(index_exports, {
75
75
  getVacuumCapabilities: () => getVacuumCapabilities,
76
76
  isDreameConsumableKey: () => isDreameConsumableKey,
77
77
  mowerConsumableIndex: () => mowerConsumableIndex,
78
+ mowerFaultSeverity: () => mowerFaultSeverity,
78
79
  parseMowerConsumables: () => parseMowerConsumables,
79
80
  parseMowerHeartbeat: () => parseMowerHeartbeat,
81
+ parseMowingProgress: () => parseMowingProgress,
80
82
  renderMowerSvg: () => renderMowerSvg,
81
83
  renderVacuumPng: () => renderVacuumPng,
82
84
  resolveCapabilities: () => resolveCapabilities,
@@ -86,7 +88,7 @@ module.exports = __toCommonJS(index_exports);
86
88
 
87
89
  // src/support/version.ts
88
90
  var LIBRARY_NAME = "nodedreame";
89
- var LIBRARY_VERSION = "1.11.5";
91
+ var LIBRARY_VERSION = "1.11.9";
90
92
 
91
93
  // src/transport/errors.ts
92
94
  var DreameError = class extends Error {
@@ -385,6 +387,11 @@ var CachedPropsResponseSchema = import_zod.z.object({
385
387
  msg: import_zod.z.string().nullish(),
386
388
  data: import_zod.z.array(CachedPropEntrySchema).optional()
387
389
  }).passthrough();
390
+ var BatchDeviceDataResponseSchema = import_zod.z.object({
391
+ code: import_zod.z.number().optional(),
392
+ msg: import_zod.z.string().nullish(),
393
+ data: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
394
+ }).passthrough();
388
395
  var SendCommandResponseSchema = import_zod.z.object({
389
396
  code: import_zod.z.number().optional(),
390
397
  msg: import_zod.z.string().nullish(),
@@ -645,6 +652,29 @@ async function callAction(base, action, opts = {}) {
645
652
  const res = await sendCommand({ ...base, ...opts, method: "action", params });
646
653
  return res.data?.result ?? res.result ?? res;
647
654
  }
655
+ async function getBatchDeviceDatas(base, props, opts = {}) {
656
+ const ctx = base.ctx ?? RequestContext.from({ ...base, host: base.apiHost });
657
+ const signal = opts.signal ?? base.signal;
658
+ const timeoutMs = opts.timeoutMs ?? base.timeoutMs;
659
+ const raw = await httpPostJsonBody({
660
+ ctx,
661
+ path: "/dreame-user-iot/iotuserdata/getDeviceData",
662
+ accessToken: base.session.accessToken,
663
+ body: { did: base.did, model: props },
664
+ context: "batch device data",
665
+ ...signal !== void 0 ? { signal } : {},
666
+ ...timeoutMs !== void 0 ? { timeoutMs } : {}
667
+ });
668
+ const parsed = BatchDeviceDataResponseSchema.parse(raw);
669
+ if (parsed.code !== void 0 && parsed.code !== 0) {
670
+ throw new DreameApiError(
671
+ `batch device data rejected: code=${parsed.code} msg=${parsed.msg ?? "?"}`,
672
+ 200,
673
+ parsed
674
+ );
675
+ }
676
+ return parsed.data ?? {};
677
+ }
648
678
  function extractResultArray(res, context) {
649
679
  const raw = Array.isArray(res.data?.result) ? res.data.result : Array.isArray(res.result) ? res.result : null;
650
680
  if (raw === null) {
@@ -2149,6 +2179,43 @@ function parseFrame(inflated) {
2149
2179
  return { header, tail };
2150
2180
  }
2151
2181
 
2182
+ // src/models/vacuum/map/segment-types.ts
2183
+ var SEGMENT_TYPE_CODE_TO_NAME = {
2184
+ 0: "Room",
2185
+ 1: "Living Room",
2186
+ 2: "Primary Bedroom",
2187
+ 3: "Study",
2188
+ 4: "Kitchen",
2189
+ 5: "Dining Hall",
2190
+ 6: "Bathroom",
2191
+ 7: "Balcony",
2192
+ 8: "Corridor",
2193
+ 9: "Utility Room",
2194
+ 10: "Closet",
2195
+ 11: "Meeting Room",
2196
+ 12: "Office",
2197
+ 13: "Fitness Area",
2198
+ 14: "Recreation Area",
2199
+ 15: "Secondary Bedroom"
2200
+ };
2201
+ function safeBase64ToUtf8(s) {
2202
+ try {
2203
+ return Buffer.from(s, "base64").toString("utf8");
2204
+ } catch {
2205
+ return null;
2206
+ }
2207
+ }
2208
+ function resolveSegmentName(type, index, customName) {
2209
+ if (type !== void 0 && type !== 0 && SEGMENT_TYPE_CODE_TO_NAME[type] !== void 0) {
2210
+ const base = SEGMENT_TYPE_CODE_TO_NAME[type];
2211
+ return index !== void 0 && index > 0 ? `${base} ${index + 1}` : base;
2212
+ }
2213
+ if (typeof customName === "string" && customName.length > 0) {
2214
+ return safeBase64ToUtf8(customName);
2215
+ }
2216
+ return null;
2217
+ }
2218
+
2152
2219
  // src/models/vacuum/map/pixel-grid.ts
2153
2220
  function classifyPixelFsm1(byte) {
2154
2221
  if (byte === 0) {
@@ -2294,7 +2361,9 @@ function collectSegments(layers, dim, tail) {
2294
2361
  };
2295
2362
  segs.push({
2296
2363
  id,
2297
- name: meta?.name ? safeBase64ToUtf8(meta.name) : null,
2364
+ // Mirror the donor: a room-type code (≠0) yields a localized default name
2365
+ // (e.g. "Kitchen"), else the user's custom base64 name, else null.
2366
+ name: resolveSegmentName(meta?.type, meta?.index, meta?.name),
2298
2367
  bbox,
2299
2368
  centroid,
2300
2369
  neighbours: meta?.nei_id ?? [],
@@ -2305,13 +2374,6 @@ function collectSegments(layers, dim, tail) {
2305
2374
  }
2306
2375
  return segs;
2307
2376
  }
2308
- function safeBase64ToUtf8(s) {
2309
- try {
2310
- return Buffer.from(s, "base64").toString("utf8");
2311
- } catch {
2312
- return null;
2313
- }
2314
- }
2315
2377
 
2316
2378
  // src/models/vacuum/map/path.ts
2317
2379
  var PATH_TYPE_FROM_OP = {
@@ -2857,18 +2919,27 @@ function decodeVacuumMap(input, opts = {}) {
2857
2919
  const pixelGrid = inflated.subarray(pixelStart, pixelEnd);
2858
2920
  const canDecodePixels = header.frameType === "I" && pixelGrid.length === header.width * header.height;
2859
2921
  const layers = canDecodePixels ? decodePixelGridFsm1(pixelGrid, header.width, header.height) : [];
2860
- const segments = canDecodePixels ? collectSegments(layers, dimensions, tail) : [];
2861
2922
  const paths = parsePathTr(tail.tr ?? "");
2862
2923
  const obstacles = parseObstacles(tail.ai_obstacle ?? []);
2863
2924
  let geometry = parseTailGeometry(tail);
2864
- if (!isGeometryComplete(geometry) && typeof tail.rism === "string" && tail.rism.length > 0) {
2925
+ let segTail = tail;
2926
+ const outerHasNames = Object.values(tail.seg_inf ?? {}).some(
2927
+ (m) => typeof m?.name === "string" && m.name.length > 0
2928
+ );
2929
+ if ((!isGeometryComplete(geometry) || !outerHasNames) && typeof tail.rism === "string" && tail.rism.length > 0) {
2865
2930
  try {
2866
2931
  const innerInflated = unwrapEnvelope(tail.rism);
2867
2932
  const { tail: innerTail } = parseFrame(innerInflated);
2868
- geometry = coalesceGeometry(geometry, parseTailGeometry(innerTail));
2933
+ if (!isGeometryComplete(geometry)) {
2934
+ geometry = coalesceGeometry(geometry, parseTailGeometry(innerTail));
2935
+ }
2936
+ if (!outerHasNames && innerTail.seg_inf) {
2937
+ segTail = mergeSegInfNames(tail, innerTail.seg_inf);
2938
+ }
2869
2939
  } catch {
2870
2940
  }
2871
2941
  }
2942
+ const segments = canDecodePixels ? collectSegments(layers, dimensions, segTail) : [];
2872
2943
  const cleanedArea = typeof tail.decmap === "string" ? parseCleanedAreaOverlay(tail.decmap) : null;
2873
2944
  return {
2874
2945
  mapId: header.mapId,
@@ -2892,6 +2963,18 @@ function applyVacuumPFrame(prev, pframe, opts = {}) {
2892
2963
  const merged = typeof prev === "string" || typeof pframe === "string" ? mergePFrameEnvelope(prev, pframe, opts.prev, opts.pframe) : mergePFrame(prev, pframe);
2893
2964
  return { buffer: merged, data: decodeVacuumMap(merged) };
2894
2965
  }
2966
+ function mergeSegInfNames(tail, innerSegInf) {
2967
+ const outer = tail.seg_inf ?? {};
2968
+ const ids = /* @__PURE__ */ new Set([...Object.keys(outer), ...Object.keys(innerSegInf)]);
2969
+ const merged = {};
2970
+ for (const id of ids) {
2971
+ const o = outer[id] ?? {};
2972
+ const inner = innerSegInf[id] ?? {};
2973
+ const name = typeof o.name === "string" && o.name.length > 0 ? o.name : inner.name;
2974
+ merged[id] = { ...inner, ...o, ...name !== void 0 ? { name } : {} };
2975
+ }
2976
+ return { ...tail, seg_inf: merged };
2977
+ }
2895
2978
  function mergeDimensions(header, tail) {
2896
2979
  const left = tail.origin?.[0] ?? header.left;
2897
2980
  const top = tail.origin?.[1] ?? header.top;
@@ -3052,8 +3135,12 @@ function renderVacuumPng(map, opts = {}) {
3052
3135
  const byType = opts.colorObstaclesByType !== false;
3053
3136
  for (const ob of map.obstacles) {
3054
3137
  const p = worldToPx(ob.x, ob.y, dim, scale);
3055
- const color = byType ? obstacleColor(ob.type) : pal.obstacle;
3056
- drawDiamond(png, p.x, p.y, Math.max(2, scale * 2), color);
3138
+ const r = Math.max(2, scale * 2);
3139
+ if (byType) {
3140
+ drawObstacleShape(png, p.x, p.y, r, ob.type, obstacleColor(ob.type));
3141
+ } else {
3142
+ drawDiamond(png, p.x, p.y, r, pal.obstacle);
3143
+ }
3057
3144
  }
3058
3145
  }
3059
3146
  if (opts.showCharger !== false && map.dock !== null) {
@@ -3214,6 +3301,32 @@ function obstacleColor(type) {
3214
3301
  const hue = Math.abs(Math.trunc(type)) * 47 % 360;
3215
3302
  return hsvToRgba(hue, 0.85, 0.95);
3216
3303
  }
3304
+ function drawSquare(png, cx, cy, r, color) {
3305
+ fillRect(png, cx - r, cy - r, cx + r, cy + r, color);
3306
+ }
3307
+ function drawTriangle(png, cx, cy, r, color) {
3308
+ for (let dy = -r; dy <= r; dy += 1) {
3309
+ const w = Math.round((dy + r) / (2 * r) * r);
3310
+ for (let dx = -w; dx <= w; dx += 1) {
3311
+ setPixel(png, cx + dx, cy + dy, color);
3312
+ }
3313
+ }
3314
+ }
3315
+ function drawObstacleShape(png, cx, cy, r, type, color) {
3316
+ switch (Math.abs(Math.trunc(type)) % 4) {
3317
+ case 0:
3318
+ drawDiamond(png, cx, cy, r, color);
3319
+ return;
3320
+ case 1:
3321
+ drawSquare(png, cx, cy, r, color);
3322
+ return;
3323
+ case 2:
3324
+ drawTriangle(png, cx, cy, r, color);
3325
+ return;
3326
+ default:
3327
+ drawDisk(png, cx, cy, r, color);
3328
+ }
3329
+ }
3217
3330
  var FURNITURE_LINE = [150, 110, 200, 235];
3218
3331
  function paintFurnitureZone(png, points, dim, scale) {
3219
3332
  if (points.length < 2) return;
@@ -4056,7 +4169,8 @@ var TASK_OPCODE = {
4056
4169
  ALL_AREA: 100,
4057
4170
  EDGE: 101,
4058
4171
  ZONE: 102,
4059
- SPOT: 103
4172
+ SPOT: 103,
4173
+ SET_CURRENT_MAP: 200
4060
4174
  };
4061
4175
  function buildResumePayload() {
4062
4176
  return { m: "a", p: 0, o: 5 };
@@ -4073,6 +4187,9 @@ function buildEdgePayload(contourIds) {
4073
4187
  function buildSpotPayload(spotAreaIds) {
4074
4188
  return { m: "a", p: 0, o: TASK_OPCODE.SPOT, d: { area: [...spotAreaIds] } };
4075
4189
  }
4190
+ function buildSetCurrentMapPayload(mapIndex) {
4191
+ return { m: "a", p: 0, o: TASK_OPCODE.SET_CURRENT_MAP, d: { idx: mapIndex } };
4192
+ }
4076
4193
  function buildGetConsumablePayload() {
4077
4194
  return { m: "g", t: "CMS" };
4078
4195
  }
@@ -4198,6 +4315,12 @@ var MowerFault = /* @__PURE__ */ ((MowerFault2) => {
4198
4315
  MowerFault2[MowerFault2["TopCoverOpen"] = 73] = "TopCoverOpen";
4199
4316
  return MowerFault2;
4200
4317
  })(MowerFault || {});
4318
+ function mowerFaultSeverity(code) {
4319
+ if (code === null) return "info";
4320
+ if (code >= 1 && code <= 30 || code === 37 || code === 73) return "error";
4321
+ if (code >= 31 && code <= 36 || code >= 38 && code <= 45) return "warning";
4322
+ return "info";
4323
+ }
4201
4324
 
4202
4325
  // src/models/mower/decode.ts
4203
4326
  function isRecord2(v) {
@@ -4408,6 +4531,37 @@ function parseMowerHeartbeat(value) {
4408
4531
  const taskSubState = mainState === HEARTBEAT_MAIN_STATE_MOWING ? MOWER_TASK_SUBSTATES[subStateRaw - HEARTBEAT_SUBSTATE_BASE] ?? null : null;
4409
4532
  return { rawBattery, mainState, subStateRaw, taskSubState };
4410
4533
  }
4534
+ var POSE_SENTINEL = 206;
4535
+ function poseBytes(value) {
4536
+ if (!Array.isArray(value)) return null;
4537
+ const bytes = [];
4538
+ for (const b of value) {
4539
+ if (typeof b !== "number") return null;
4540
+ bytes.push(b);
4541
+ }
4542
+ return bytes;
4543
+ }
4544
+ function parseMowingProgress(value) {
4545
+ const bytes = poseBytes(value);
4546
+ if (bytes === null || bytes.length < 11) return null;
4547
+ const last = bytes[bytes.length - 1];
4548
+ const head = bytes[0];
4549
+ let offset = null;
4550
+ if (head !== POSE_SENTINEL && last === POSE_SENTINEL) {
4551
+ offset = 0;
4552
+ } else if (head === POSE_SENTINEL && last === POSE_SENTINEL && bytes.length >= 33) {
4553
+ offset = 22;
4554
+ }
4555
+ if (offset === null || offset + 10 > bytes.length) return null;
4556
+ const rawPercent = (bytes[offset + 2] ?? 0) | (bytes[offset + 3] ?? 0) << 8;
4557
+ const total = (bytes[offset + 4] ?? 0) | (bytes[offset + 5] ?? 0) << 8 | (bytes[offset + 6] ?? 0) << 16;
4558
+ const finish = (bytes[offset + 7] ?? 0) | (bytes[offset + 8] ?? 0) << 8 | (bytes[offset + 9] ?? 0) << 16;
4559
+ return {
4560
+ progressPercent: rawPercent ? Math.min(100, rawPercent / 100) : 0,
4561
+ currentAreaSqm: finish / 100,
4562
+ totalAreaSqm: total / 100
4563
+ };
4564
+ }
4411
4565
 
4412
4566
  // src/models/mower/capabilities.ts
4413
4567
  var FALLBACK2 = {
@@ -5026,7 +5180,10 @@ var MowerDevice = class _MowerDevice extends BaseDevice {
5026
5180
  capabilities: input.capabilities ?? new MowerCapabilityResolver().resolve(input.device.model)
5027
5181
  });
5028
5182
  this.#caps = getMowerCapabilities(input.device.model);
5029
- this.#fetchBatch = input.getBatchDeviceDatas ?? null;
5183
+ this.#fetchBatch = input.getBatchDeviceDatas ?? ((did, props) => getBatchDeviceDatas(
5184
+ { session: this.currentSession(), region: this.region, did },
5185
+ props
5186
+ ));
5030
5187
  }
5031
5188
  /** Rich, mower-specific capability record. */
5032
5189
  get mowerCapabilities() {
@@ -5107,6 +5264,25 @@ var MowerDevice = class _MowerDevice extends BaseDevice {
5107
5264
  get coverageTargetPct() {
5108
5265
  return this.task?.coverageTarget ?? null;
5109
5266
  }
5267
+ /**
5268
+ * Live mowing PROGRESS decoded from the POSE_COVERAGE (1:4) task block —
5269
+ * `{progressPercent, currentAreaSqm, totalAreaSqm}` — or null when 1:4 carries
5270
+ * no task block (idle / pose-only frame). This is the byte-accurate progress
5271
+ * the Dreamehome app shows (vs {@link coverageTargetPct}, the task TARGET).
5272
+ */
5273
+ get mowingProgress() {
5274
+ return parseMowingProgress(
5275
+ this.getProperty(MOWER_PROP.POSE_COVERAGE.siid, MOWER_PROP.POSE_COVERAGE.piid)?.value
5276
+ );
5277
+ }
5278
+ /** Mowing completion percent (0..100) from {@link mowingProgress}, or null. */
5279
+ get mowingProgressPct() {
5280
+ return this.mowingProgress?.progressPercent ?? null;
5281
+ }
5282
+ /** Severity of the current DEVICE_CODE (2:2) — info / warning / error. */
5283
+ get faultSeverity() {
5284
+ return mowerFaultSeverity(this.faultRaw);
5285
+ }
5110
5286
  /** Parsed per-zone control status (2:56), or null. */
5111
5287
  get controlStatus() {
5112
5288
  return parseControlStatus(
@@ -5186,6 +5362,20 @@ var MowerDevice = class _MowerDevice extends BaseDevice {
5186
5362
  }
5187
5363
  return this.#sendTask(buildSpotPayload(spotAreaIds.map((s) => Math.trunc(s))));
5188
5364
  }
5365
+ /**
5366
+ * Switch the mower's ACTIVE map (2:50 o:200). Takes a map *id* (as carried by
5367
+ * {@link MowerMap.availableMaps}/`currentMapId`) and resolves it to the
5368
+ * firmware's map *index* via the last-fetched map; falls back to treating the
5369
+ * id as the index when no map has been fetched yet. Mirrors the donor
5370
+ * `set_current_map`. Capability-gated on `canMap`.
5371
+ */
5372
+ async setCurrentMap(mapId) {
5373
+ this.#requireCap(this.#caps.canMap, "setCurrentMap", "map switching");
5374
+ const id = Math.trunc(mapId);
5375
+ const entry = this.#lastMap?.availableMaps.find((m) => m.mapId === id);
5376
+ const index = entry?.mapIndex ?? id;
5377
+ return this.#sendTask(buildSetCurrentMapPayload(index));
5378
+ }
5189
5379
  // -- CMS consumables ----------------------------------------------------
5190
5380
  /**
5191
5381
  * Read the raw CMS consumable counters `[blade, brush, robot]` (minutes used),
@@ -5286,6 +5476,7 @@ var MowerDevice = class _MowerDevice extends BaseDevice {
5286
5476
  /** Props worth seeding on start() / polling — exported for the facade. */
5287
5477
  static DEFAULT_PROPS = [
5288
5478
  MOWER_PROP.HEARTBEAT,
5479
+ MOWER_PROP.POSE_COVERAGE,
5289
5480
  MOWER_PROP.STATUS,
5290
5481
  MOWER_PROP.DEVICE_CODE,
5291
5482
  MOWER_PROP.BATTERY,
@@ -5945,8 +6136,10 @@ function createClientDumper(client, options) {
5945
6136
  getVacuumCapabilities,
5946
6137
  isDreameConsumableKey,
5947
6138
  mowerConsumableIndex,
6139
+ mowerFaultSeverity,
5948
6140
  parseMowerConsumables,
5949
6141
  parseMowerHeartbeat,
6142
+ parseMowingProgress,
5950
6143
  renderMowerSvg,
5951
6144
  renderVacuumPng,
5952
6145
  resolveCapabilities,