@apocaliss92/nodedreame 1.4.0 → 1.6.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.cjs CHANGED
@@ -30,6 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ AI_FEATURE_BIT: () => AI_FEATURE_BIT,
34
+ AI_FEATURE_JSON_KEY: () => AI_FEATURE_JSON_KEY,
33
35
  BaseDevice: () => BaseDevice,
34
36
  ChargingStatus: () => ChargingStatus,
35
37
  CleaningMode: () => CleaningMode,
@@ -59,6 +61,8 @@ __export(index_exports, {
59
61
  WaterVolume: () => WaterVolume,
60
62
  createClientDumper: () => createClientDumper,
61
63
  createDumper: () => createDumper,
64
+ decodeAiFeature: () => decodeAiFeature,
65
+ encodeAiFeatureWrite: () => encodeAiFeatureWrite,
62
66
  getMowerCapabilities: () => getMowerCapabilities,
63
67
  getVacuumCapabilities: () => getVacuumCapabilities,
64
68
  renderMowerSvg: () => renderMowerSvg,
@@ -69,7 +73,7 @@ module.exports = __toCommonJS(index_exports);
69
73
 
70
74
  // src/support/version.ts
71
75
  var LIBRARY_NAME = "nodedreame";
72
- var LIBRARY_VERSION = "1.3.1";
76
+ var LIBRARY_VERSION = "1.6.0";
73
77
 
74
78
  // src/transport/errors.ts
75
79
  var DreameError = class extends Error {
@@ -1218,6 +1222,14 @@ var VACUUM_PROP = {
1218
1222
  CLEANING_MODE: { siid: 4, piid: 23 },
1219
1223
  /** VERIFIED r2532a — Child Lock boolean. */
1220
1224
  CHILD_LOCK: { siid: 4, piid: 27 },
1225
+ /**
1226
+ * AI obstacle-detection toggle bundle (Tasshack types.py:1609 — `AI_DETECTION`,
1227
+ * siid 4 piid 22). The value packs ALL AI-obstacle switches into ONE property,
1228
+ * encoded as EITHER an int bitmask (`DreameVacuumAIProperty`) OR a JSON string
1229
+ * (`DreameVacuumStrAIProperty`) depending on firmware. Decode/encode per-feature
1230
+ * via `ai-detection.ts` ({@link decodeAiFeature} / {@link encodeAiFeatureWrite}).
1231
+ */
1232
+ AI_DETECTION: { siid: 4, piid: 22 },
1221
1233
  /** VERIFIED r2532a 2026-05-03 — task progress percentage 0..100. */
1222
1234
  TASK_PROGRESS_PCT: { siid: 4, piid: 63 },
1223
1235
  /** VERIFIED r2532a — mop-drying progress (minutes ticking during MopDrying). */
@@ -1556,7 +1568,91 @@ function parseFaultList(value) {
1556
1568
  return codes;
1557
1569
  }
1558
1570
 
1571
+ // src/models/vacuum/ai-detection.ts
1572
+ var AI_FEATURE_BIT = {
1573
+ furnitureDetection: 1,
1574
+ obstacleDetection: 2,
1575
+ obstaclePicture: 4,
1576
+ fluidDetection: 8,
1577
+ petDetection: 16,
1578
+ obstacleImageUpload: 32,
1579
+ // AI_IMAGE = 64 is an internal flag, not a user toggle — intentionally omitted.
1580
+ petAvoidance: 128,
1581
+ fuzzyObstacleDetection: 256,
1582
+ petPicture: 512,
1583
+ petFocusedDetection: 1024,
1584
+ largeParticlesBoost: 2048
1585
+ };
1586
+ var AI_FEATURE_JSON_KEY = {
1587
+ obstacleDetection: "obstacle_detect_switch",
1588
+ obstacleImageUpload: "obstacle_app_display_switch",
1589
+ petDetection: "whether_have_pet",
1590
+ humanDetection: "human_detect_switch",
1591
+ furnitureDetection: "furniture_detect_switch",
1592
+ fluidDetection: "fluid_detect_switch"
1593
+ };
1594
+ function isRecord(v) {
1595
+ return v !== null && typeof v === "object" && !Array.isArray(v);
1596
+ }
1597
+ function parseJsonPayload(raw) {
1598
+ try {
1599
+ const parsed = JSON.parse(raw);
1600
+ return isRecord(parsed) ? parsed : null;
1601
+ } catch {
1602
+ return null;
1603
+ }
1604
+ }
1605
+ function coerceBool(v) {
1606
+ if (typeof v === "boolean") return v;
1607
+ if (v === 1 || v === "1") return true;
1608
+ if (v === 0 || v === "0") return false;
1609
+ return null;
1610
+ }
1611
+ function decodeAiFeature(raw, feature) {
1612
+ if (typeof raw === "number") {
1613
+ const bit = AI_FEATURE_BIT[feature];
1614
+ if (bit === void 0) return null;
1615
+ return (raw & bit) === bit;
1616
+ }
1617
+ if (typeof raw === "string") {
1618
+ const key2 = AI_FEATURE_JSON_KEY[feature];
1619
+ if (key2 === void 0) return null;
1620
+ const payload = parseJsonPayload(raw);
1621
+ if (payload === null) return null;
1622
+ return coerceBool(payload[key2]);
1623
+ }
1624
+ return null;
1625
+ }
1626
+ function encodeAiFeatureWrite(raw, feature, value) {
1627
+ if (typeof raw === "number") {
1628
+ const bit = AI_FEATURE_BIT[feature];
1629
+ if (bit === void 0) {
1630
+ throw new Error(`encodeAiFeatureWrite: feature "${feature}" has no int-bitmask representation`);
1631
+ }
1632
+ return value ? raw | bit : raw & ~bit;
1633
+ }
1634
+ if (typeof raw === "string") {
1635
+ const key2 = AI_FEATURE_JSON_KEY[feature];
1636
+ if (key2 === void 0) {
1637
+ throw new Error(`encodeAiFeatureWrite: feature "${feature}" has no JSON-key representation`);
1638
+ }
1639
+ return JSON.stringify({ [key2]: value });
1640
+ }
1641
+ throw new Error(
1642
+ `encodeAiFeatureWrite: AI_DETECTION value not yet known \u2014 cannot safely toggle "${feature}"`
1643
+ );
1644
+ }
1645
+
1559
1646
  // src/models/vacuum/capabilities.ts
1647
+ var X50_AI_FEATURES = [
1648
+ "obstacleDetection",
1649
+ "petDetection",
1650
+ "furnitureDetection",
1651
+ "fluidDetection",
1652
+ "obstaclePicture",
1653
+ "obstacleImageUpload",
1654
+ "fuzzyObstacleDetection"
1655
+ ];
1560
1656
  var FALLBACK = {
1561
1657
  verified: false,
1562
1658
  canMop: false,
@@ -1585,7 +1681,9 @@ var FALLBACK = {
1585
1681
  2 /* Intense */,
1586
1682
  3 /* Max */
1587
1683
  ],
1588
- supportedWaterVolumes: [1 /* Low */, 2 /* Medium */, 3 /* High */]
1684
+ supportedWaterVolumes: [1 /* Low */, 2 /* Medium */, 3 /* High */],
1685
+ // An unknown model advertises no AI toggles until its feature set is verified.
1686
+ supportedAiFeatures: []
1589
1687
  };
1590
1688
  var X50_FAMILY = {
1591
1689
  canMop: true,
@@ -1611,7 +1709,8 @@ var X50_FAMILY = {
1611
1709
  2 /* Intense */,
1612
1710
  3 /* Max */
1613
1711
  ],
1614
- supportedWaterVolumes: [1 /* Low */, 2 /* Medium */, 3 /* High */]
1712
+ supportedWaterVolumes: [1 /* Low */, 2 /* Medium */, 3 /* High */],
1713
+ supportedAiFeatures: X50_AI_FEATURES
1615
1714
  };
1616
1715
  function deepFreeze(obj) {
1617
1716
  if (obj && typeof obj === "object" && !Object.isFrozen(obj)) {
@@ -2745,6 +2844,39 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
2745
2844
  get volume() {
2746
2845
  return this.#num(SETTINGS_PROP.VOLUME.siid, SETTINGS_PROP.VOLUME.piid);
2747
2846
  }
2847
+ // -- AI obstacle-detection ----------------------------------------------
2848
+ /**
2849
+ * The raw `AI_DETECTION` value (siid 4 piid 22) — an int bitmask or a JSON
2850
+ * string, depending on firmware — or `null` when not yet observed. Prefer the
2851
+ * decoded {@link aiFeature} / {@link supportedAiFeatures} surface.
2852
+ */
2853
+ get aiDetectionRaw() {
2854
+ const v = this.getProperty(VACUUM_PROP.AI_DETECTION.siid, VACUUM_PROP.AI_DETECTION.piid)?.value;
2855
+ return typeof v === "number" || typeof v === "string" ? v : null;
2856
+ }
2857
+ /** The AI-obstacle toggles this model exposes (from its capability record). */
2858
+ get supportedAiFeatures() {
2859
+ return this.#caps.supportedAiFeatures;
2860
+ }
2861
+ /**
2862
+ * Read ONE AI-obstacle toggle's on/off state, decoded from the packed
2863
+ * `AI_DETECTION` value (transparent across the int / JSON encodings). `null`
2864
+ * when the value is unobserved or the feature is not representable.
2865
+ */
2866
+ aiFeature(feature) {
2867
+ return decodeAiFeature(this.aiDetectionRaw, feature);
2868
+ }
2869
+ /**
2870
+ * Toggle ONE AI-obstacle feature, preserving the others via a read-modify-write
2871
+ * of the packed `AI_DETECTION` property (mirrors Tasshack `set_ai_detection`).
2872
+ * Capability-gated on `hasAiObstacleDetection`; rejects when the current value
2873
+ * is unknown (a blind write would clobber the other toggles).
2874
+ */
2875
+ async setAiFeature(feature, value) {
2876
+ this.#requireCap(this.#caps.hasAiObstacleDetection, "setAiFeature", "AI obstacle detection");
2877
+ const next = encodeAiFeatureWrite(this.aiDetectionRaw, feature, value);
2878
+ return this.setProperty({ ...VACUUM_PROP.AI_DETECTION, value: next });
2879
+ }
2748
2880
  // -- command helpers ----------------------------------------------------
2749
2881
  #resolveCleanOpts(opts) {
2750
2882
  const repeats = Math.max(1, Math.trunc(opts.repeats ?? 1));
@@ -2908,6 +3040,23 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
2908
3040
  if (filename === null) return null;
2909
3041
  return this.getMap({ filename, ...opts });
2910
3042
  }
3043
+ /**
3044
+ * Seed {@link mapFilename} from the CLOUD SHADOW (the last value the robot
3045
+ * pushed for siid 6 piid 3) WITHOUT waking it — so a docked/idle robot can
3046
+ * surface its LAST cleaning map without a fresh clean. Emits the usual
3047
+ * `propertyChanged`/`stateChanged` (so a map-watching consumer re-renders) and
3048
+ * returns the resolved {@link mapFilename} (null when the model has no map or
3049
+ * the shadow carries no map path yet).
3050
+ *
3051
+ * NOTE: the shadow holds the last LIVE-PATH filename; its OSS blob may have
3052
+ * expired, in which case a follow-up {@link fetchLatestMap} rejects — treat
3053
+ * that as "no saved map available".
3054
+ */
3055
+ async refreshSavedMapFilename() {
3056
+ if (!this.#caps.canMap) return null;
3057
+ await this.refreshCachedProperties([VACUUM_PROP.MAP_PATH]);
3058
+ return this.mapFilename;
3059
+ }
2911
3060
  /**
2912
3061
  * The current room/segment id, derived from the most-recently-decoded map's
2913
3062
  * active-segment set (`sa`). `null` when no map has been fetched or no
@@ -2963,6 +3112,7 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
2963
3112
  VACUUM_PROP.WATER_VOLUME,
2964
3113
  VACUUM_PROP.CLEAN_MODE_SETTING,
2965
3114
  VACUUM_PROP.TASK_PROGRESS_PCT,
3115
+ VACUUM_PROP.AI_DETECTION,
2966
3116
  BATTERY_PROP.LEVEL,
2967
3117
  BATTERY_PROP.CHARGING_STATUS,
2968
3118
  CONSUMABLE_PROP.MAIN_BRUSH_LEFT,
@@ -3135,7 +3285,7 @@ var MowerFault = /* @__PURE__ */ ((MowerFault2) => {
3135
3285
  })(MowerFault || {});
3136
3286
 
3137
3287
  // src/models/mower/decode.ts
3138
- function isRecord(v) {
3288
+ function isRecord2(v) {
3139
3289
  return typeof v === "object" && v !== null && !Array.isArray(v);
3140
3290
  }
3141
3291
  function numArrayOrNull(v) {
@@ -3152,12 +3302,12 @@ function numArrayOrNull(v) {
3152
3302
  return out;
3153
3303
  }
3154
3304
  function parseTaskDescriptor(value) {
3155
- if (!isRecord(value)) {
3305
+ if (!isRecord2(value)) {
3156
3306
  return null;
3157
3307
  }
3158
3308
  const t = value["t"];
3159
3309
  const d = value["d"];
3160
- if (typeof t !== "string" || !isRecord(d)) {
3310
+ if (typeof t !== "string" || !isRecord2(d)) {
3161
3311
  return null;
3162
3312
  }
3163
3313
  const exe = d["exe"];
@@ -3186,7 +3336,7 @@ function controlActionFor(code) {
3186
3336
  return controlLookup(code);
3187
3337
  }
3188
3338
  function parseControlStatus(value) {
3189
- if (!isRecord(value)) {
3339
+ if (!isRecord2(value)) {
3190
3340
  return null;
3191
3341
  }
3192
3342
  const status = value["status"];
@@ -3296,7 +3446,7 @@ var MowerCapabilityResolver = class {
3296
3446
  // src/models/mower/map/parser.ts
3297
3447
  var PATH_SENTINEL_X = 32767;
3298
3448
  var PATH_SENTINEL_Y = -32768;
3299
- function isRecord2(v) {
3449
+ function isRecord3(v) {
3300
3450
  return typeof v === "object" && v !== null && !Array.isArray(v);
3301
3451
  }
3302
3452
  function asNumber(v, fallback) {
@@ -3334,7 +3484,7 @@ function parseSplitPos(info) {
3334
3484
  return 0;
3335
3485
  }
3336
3486
  function parsePolygonList(dataMap) {
3337
- if (!isRecord2(dataMap) || dataMap["dataType"] !== "Map") {
3487
+ if (!isRecord3(dataMap) || dataMap["dataType"] !== "Map") {
3338
3488
  return [];
3339
3489
  }
3340
3490
  const value = dataMap["value"];
@@ -3346,7 +3496,7 @@ function extractPathCoords(pathList) {
3346
3496
  }
3347
3497
  const out = [];
3348
3498
  for (const p of pathList) {
3349
- if (isRecord2(p) && typeof p["x"] === "number" && typeof p["y"] === "number") {
3499
+ if (isRecord3(p) && typeof p["x"] === "number" && typeof p["y"] === "number") {
3350
3500
  out.push({ x: p["x"], y: p["y"] });
3351
3501
  }
3352
3502
  }
@@ -3381,7 +3531,7 @@ function asEntry(entry) {
3381
3531
  }
3382
3532
  const tuple = entry;
3383
3533
  const data = tuple[1];
3384
- if (!isRecord2(data)) {
3534
+ if (!isRecord3(data)) {
3385
3535
  return null;
3386
3536
  }
3387
3537
  return { id: tuple[0], data };
@@ -3459,7 +3609,7 @@ function parseContours(list) {
3459
3609
  return out;
3460
3610
  }
3461
3611
  function parseBoundary(raw) {
3462
- if (!isRecord2(raw)) {
3612
+ if (!isRecord3(raw)) {
3463
3613
  return null;
3464
3614
  }
3465
3615
  const { x1, y1, x2, y2 } = raw;
@@ -3470,7 +3620,7 @@ function parseBoundary(raw) {
3470
3620
  }
3471
3621
  function parseMowerMap(mapJsonStr) {
3472
3622
  const data = JSON.parse(mapJsonStr);
3473
- if (!isRecord2(data)) {
3623
+ if (!isRecord3(data)) {
3474
3624
  throw new Error("Map JSON is not an object");
3475
3625
  }
3476
3626
  const mapIndex = asNumber(data["mapIndex"], 0);
@@ -4640,6 +4790,8 @@ function createClientDumper(client, options) {
4640
4790
  }
4641
4791
  // Annotate the CommonJS export names for ESM import in node:
4642
4792
  0 && (module.exports = {
4793
+ AI_FEATURE_BIT,
4794
+ AI_FEATURE_JSON_KEY,
4643
4795
  BaseDevice,
4644
4796
  ChargingStatus,
4645
4797
  CleaningMode,
@@ -4669,6 +4821,8 @@ function createClientDumper(client, options) {
4669
4821
  WaterVolume,
4670
4822
  createClientDumper,
4671
4823
  createDumper,
4824
+ decodeAiFeature,
4825
+ encodeAiFeatureWrite,
4672
4826
  getMowerCapabilities,
4673
4827
  getVacuumCapabilities,
4674
4828
  renderMowerSvg,