@apocaliss92/nodedreame 1.5.0 → 1.6.1

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.1";
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,53 @@ 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
+ * Seed {@link aiDetectionRaw} from the CLOUD SHADOW (the last value the robot
2871
+ * pushed for `AI_DETECTION`, siid 4 piid 22) WITHOUT waking it. The AI-obstacle
2872
+ * toggles are STATIC settings the robot rarely re-pushes over MQTT, so a fresh
2873
+ * connect to an idle robot has no value cached — call this on activate to
2874
+ * surface the current toggles. Emits the usual `propertyChanged`/`stateChanged`
2875
+ * (so a watching consumer re-decodes) and returns the resolved raw value
2876
+ * (`null` when the model has no AI detection or the shadow carries none yet).
2877
+ */
2878
+ async refreshAiDetection() {
2879
+ if (!this.#caps.hasAiObstacleDetection) return null;
2880
+ await this.refreshCachedProperties([VACUUM_PROP.AI_DETECTION]);
2881
+ return this.aiDetectionRaw;
2882
+ }
2883
+ /**
2884
+ * Toggle ONE AI-obstacle feature, preserving the others via a read-modify-write
2885
+ * of the packed `AI_DETECTION` property (mirrors Tasshack `set_ai_detection`).
2886
+ * Capability-gated on `hasAiObstacleDetection`; rejects when the current value
2887
+ * is unknown (a blind write would clobber the other toggles).
2888
+ */
2889
+ async setAiFeature(feature, value) {
2890
+ this.#requireCap(this.#caps.hasAiObstacleDetection, "setAiFeature", "AI obstacle detection");
2891
+ const next = encodeAiFeatureWrite(this.aiDetectionRaw, feature, value);
2892
+ return this.setProperty({ ...VACUUM_PROP.AI_DETECTION, value: next });
2893
+ }
2748
2894
  // -- command helpers ----------------------------------------------------
2749
2895
  #resolveCleanOpts(opts) {
2750
2896
  const repeats = Math.max(1, Math.trunc(opts.repeats ?? 1));
@@ -2980,6 +3126,7 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
2980
3126
  VACUUM_PROP.WATER_VOLUME,
2981
3127
  VACUUM_PROP.CLEAN_MODE_SETTING,
2982
3128
  VACUUM_PROP.TASK_PROGRESS_PCT,
3129
+ VACUUM_PROP.AI_DETECTION,
2983
3130
  BATTERY_PROP.LEVEL,
2984
3131
  BATTERY_PROP.CHARGING_STATUS,
2985
3132
  CONSUMABLE_PROP.MAIN_BRUSH_LEFT,
@@ -3152,7 +3299,7 @@ var MowerFault = /* @__PURE__ */ ((MowerFault2) => {
3152
3299
  })(MowerFault || {});
3153
3300
 
3154
3301
  // src/models/mower/decode.ts
3155
- function isRecord(v) {
3302
+ function isRecord2(v) {
3156
3303
  return typeof v === "object" && v !== null && !Array.isArray(v);
3157
3304
  }
3158
3305
  function numArrayOrNull(v) {
@@ -3169,12 +3316,12 @@ function numArrayOrNull(v) {
3169
3316
  return out;
3170
3317
  }
3171
3318
  function parseTaskDescriptor(value) {
3172
- if (!isRecord(value)) {
3319
+ if (!isRecord2(value)) {
3173
3320
  return null;
3174
3321
  }
3175
3322
  const t = value["t"];
3176
3323
  const d = value["d"];
3177
- if (typeof t !== "string" || !isRecord(d)) {
3324
+ if (typeof t !== "string" || !isRecord2(d)) {
3178
3325
  return null;
3179
3326
  }
3180
3327
  const exe = d["exe"];
@@ -3203,7 +3350,7 @@ function controlActionFor(code) {
3203
3350
  return controlLookup(code);
3204
3351
  }
3205
3352
  function parseControlStatus(value) {
3206
- if (!isRecord(value)) {
3353
+ if (!isRecord2(value)) {
3207
3354
  return null;
3208
3355
  }
3209
3356
  const status = value["status"];
@@ -3313,7 +3460,7 @@ var MowerCapabilityResolver = class {
3313
3460
  // src/models/mower/map/parser.ts
3314
3461
  var PATH_SENTINEL_X = 32767;
3315
3462
  var PATH_SENTINEL_Y = -32768;
3316
- function isRecord2(v) {
3463
+ function isRecord3(v) {
3317
3464
  return typeof v === "object" && v !== null && !Array.isArray(v);
3318
3465
  }
3319
3466
  function asNumber(v, fallback) {
@@ -3351,7 +3498,7 @@ function parseSplitPos(info) {
3351
3498
  return 0;
3352
3499
  }
3353
3500
  function parsePolygonList(dataMap) {
3354
- if (!isRecord2(dataMap) || dataMap["dataType"] !== "Map") {
3501
+ if (!isRecord3(dataMap) || dataMap["dataType"] !== "Map") {
3355
3502
  return [];
3356
3503
  }
3357
3504
  const value = dataMap["value"];
@@ -3363,7 +3510,7 @@ function extractPathCoords(pathList) {
3363
3510
  }
3364
3511
  const out = [];
3365
3512
  for (const p of pathList) {
3366
- if (isRecord2(p) && typeof p["x"] === "number" && typeof p["y"] === "number") {
3513
+ if (isRecord3(p) && typeof p["x"] === "number" && typeof p["y"] === "number") {
3367
3514
  out.push({ x: p["x"], y: p["y"] });
3368
3515
  }
3369
3516
  }
@@ -3398,7 +3545,7 @@ function asEntry(entry) {
3398
3545
  }
3399
3546
  const tuple = entry;
3400
3547
  const data = tuple[1];
3401
- if (!isRecord2(data)) {
3548
+ if (!isRecord3(data)) {
3402
3549
  return null;
3403
3550
  }
3404
3551
  return { id: tuple[0], data };
@@ -3476,7 +3623,7 @@ function parseContours(list) {
3476
3623
  return out;
3477
3624
  }
3478
3625
  function parseBoundary(raw) {
3479
- if (!isRecord2(raw)) {
3626
+ if (!isRecord3(raw)) {
3480
3627
  return null;
3481
3628
  }
3482
3629
  const { x1, y1, x2, y2 } = raw;
@@ -3487,7 +3634,7 @@ function parseBoundary(raw) {
3487
3634
  }
3488
3635
  function parseMowerMap(mapJsonStr) {
3489
3636
  const data = JSON.parse(mapJsonStr);
3490
- if (!isRecord2(data)) {
3637
+ if (!isRecord3(data)) {
3491
3638
  throw new Error("Map JSON is not an object");
3492
3639
  }
3493
3640
  const mapIndex = asNumber(data["mapIndex"], 0);
@@ -4657,6 +4804,8 @@ function createClientDumper(client, options) {
4657
4804
  }
4658
4805
  // Annotate the CommonJS export names for ESM import in node:
4659
4806
  0 && (module.exports = {
4807
+ AI_FEATURE_BIT,
4808
+ AI_FEATURE_JSON_KEY,
4660
4809
  BaseDevice,
4661
4810
  ChargingStatus,
4662
4811
  CleaningMode,
@@ -4686,6 +4835,8 @@ function createClientDumper(client, options) {
4686
4835
  WaterVolume,
4687
4836
  createClientDumper,
4688
4837
  createDumper,
4838
+ decodeAiFeature,
4839
+ encodeAiFeatureWrite,
4689
4840
  getMowerCapabilities,
4690
4841
  getVacuumCapabilities,
4691
4842
  renderMowerSvg,