@apocaliss92/nodedreame 1.11.0 → 1.11.2

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
@@ -44,6 +44,7 @@ __export(index_exports, {
44
44
  DreameTransportError: () => DreameTransportError,
45
45
  LIBRARY_NAME: () => LIBRARY_NAME,
46
46
  MOWER_MODEL_CAPABILITIES: () => MODEL_CAPABILITIES2,
47
+ MOWER_TASK_SUBSTATES: () => MOWER_TASK_SUBSTATES,
47
48
  MiotError: () => MiotError,
48
49
  MiotState: () => MiotState,
49
50
  MowerCapabilityResolver: () => MowerCapabilityResolver,
@@ -75,6 +76,7 @@ __export(index_exports, {
75
76
  isDreameConsumableKey: () => isDreameConsumableKey,
76
77
  mowerConsumableIndex: () => mowerConsumableIndex,
77
78
  parseMowerConsumables: () => parseMowerConsumables,
79
+ parseMowerHeartbeat: () => parseMowerHeartbeat,
78
80
  renderMowerSvg: () => renderMowerSvg,
79
81
  renderVacuumPng: () => renderVacuumPng,
80
82
  resolveCapabilities: () => resolveCapabilities,
@@ -84,7 +86,7 @@ module.exports = __toCommonJS(index_exports);
84
86
 
85
87
  // src/support/version.ts
86
88
  var LIBRARY_NAME = "nodedreame";
87
- var LIBRARY_VERSION = "1.11.0";
89
+ var LIBRARY_VERSION = "1.11.2";
88
90
 
89
91
  // src/transport/errors.ts
90
92
  var DreameError = class extends Error {
@@ -700,6 +702,7 @@ var DreamePush = class extends TypedEmitter {
700
702
  #region;
701
703
  #connect;
702
704
  #backoffMs;
705
+ #keepaliveSeconds;
703
706
  #rejectUnauthorized;
704
707
  #topic;
705
708
  #client = null;
@@ -720,6 +723,7 @@ var DreamePush = class extends TypedEmitter {
720
723
  this.#region = input.region;
721
724
  this.#connect = input.connect ?? defaultConnect;
722
725
  this.#backoffMs = input.reconnectBackoffMs ?? 5e3;
726
+ this.#keepaliveSeconds = input.keepaliveSeconds ?? 60;
723
727
  this.#rejectUnauthorized = input.rejectUnauthorized ?? false;
724
728
  this.#topic = buildStatusTopic(this.#device, this.#session.uid, this.#region);
725
729
  this.#onClose = () => {
@@ -777,6 +781,7 @@ var DreamePush = class extends TypedEmitter {
777
781
  reconnectPeriod: 0,
778
782
  // we drive reconnect ourselves
779
783
  connectTimeout: 15e3,
784
+ keepalive: this.#keepaliveSeconds,
780
785
  rejectUnauthorized: this.#rejectUnauthorized,
781
786
  clean: true
782
787
  });
@@ -784,25 +789,37 @@ var DreamePush = class extends TypedEmitter {
784
789
  client.on("message", (_topic, payload) => this.#handleMessage(payload));
785
790
  client.on("error", (err) => this.emit("error", err));
786
791
  client.on("close", this.#onClose);
787
- await new Promise((resolve, reject) => {
788
- const onConnect = () => {
789
- client.removeListener("error", onError);
790
- client.subscribe(this.#topic, { qos: 0 }, (err) => {
791
- if (err) {
792
- reject(new DreameTransportError(`mqtt subscribe failed: ${err.message}`, err));
793
- return;
794
- }
795
- this.emit("connect");
796
- resolve();
797
- });
798
- };
799
- const onError = (err) => {
800
- client.removeListener("connect", onConnect);
801
- reject(new DreameTransportError(`mqtt connect failed: ${err.message}`, err));
802
- };
803
- client.once("connect", onConnect);
804
- client.once("error", onError);
805
- });
792
+ try {
793
+ await new Promise((resolve, reject) => {
794
+ const onConnect = () => {
795
+ client.removeListener("error", onError);
796
+ client.subscribe(this.#topic, { qos: 0 }, (err) => {
797
+ if (err) {
798
+ reject(new DreameTransportError(`mqtt subscribe failed: ${err.message}`, err));
799
+ return;
800
+ }
801
+ this.emit("connect");
802
+ resolve();
803
+ });
804
+ };
805
+ const onError = (err) => {
806
+ client.removeListener("connect", onConnect);
807
+ reject(new DreameTransportError(`mqtt connect failed: ${err.message}`, err));
808
+ };
809
+ client.once("connect", onConnect);
810
+ client.once("error", onError);
811
+ });
812
+ } catch (err) {
813
+ client.removeListener("close", this.#onClose);
814
+ try {
815
+ client.end(true, {}, () => void 0);
816
+ } catch {
817
+ }
818
+ if (this.#client === client) {
819
+ this.#client = null;
820
+ }
821
+ throw err;
822
+ }
806
823
  }
807
824
  #scheduleReconnect() {
808
825
  if (this.#closed || this.#reconnectTimer) {
@@ -1644,7 +1661,9 @@ function encodeAiFeatureWrite(raw, feature, value) {
1644
1661
  if (typeof raw === "number") {
1645
1662
  const bit = AI_FEATURE_BIT[feature];
1646
1663
  if (bit === void 0) {
1647
- throw new Error(`encodeAiFeatureWrite: feature "${feature}" has no int-bitmask representation`);
1664
+ throw new Error(
1665
+ `encodeAiFeatureWrite: feature "${feature}" has no int-bitmask representation`
1666
+ );
1648
1667
  }
1649
1668
  return value ? raw | bit : raw & ~bit;
1650
1669
  }
@@ -1761,18 +1780,53 @@ function encodeAutoSwitchWrite(key2, value) {
1761
1780
 
1762
1781
  // src/models/vacuum/consumables.ts
1763
1782
  var VACUUM_CONSUMABLES = [
1764
- { key: "main-brush", label: "Main Brush", life: { siid: 9, piid: 2 }, reset: { siid: 9, aiid: 1 } },
1765
- { key: "side-brush", label: "Side Brush", life: { siid: 10, piid: 2 }, reset: { siid: 10, aiid: 1 } },
1783
+ {
1784
+ key: "main-brush",
1785
+ label: "Main Brush",
1786
+ life: { siid: 9, piid: 2 },
1787
+ reset: { siid: 9, aiid: 1 }
1788
+ },
1789
+ {
1790
+ key: "side-brush",
1791
+ label: "Side Brush",
1792
+ life: { siid: 10, piid: 2 },
1793
+ reset: { siid: 10, aiid: 1 }
1794
+ },
1766
1795
  { key: "filter", label: "Filter", life: { siid: 11, piid: 1 }, reset: { siid: 11, aiid: 1 } },
1767
1796
  { key: "sensor", label: "Sensor", life: { siid: 16, piid: 1 }, reset: { siid: 16, aiid: 1 } },
1768
- { key: "tank-filter", label: "Tank Filter", life: { siid: 17, piid: 1 }, reset: { siid: 17, aiid: 1 } },
1797
+ {
1798
+ key: "tank-filter",
1799
+ label: "Tank Filter",
1800
+ life: { siid: 17, piid: 1 },
1801
+ reset: { siid: 17, aiid: 1 }
1802
+ },
1769
1803
  { key: "mop-pad", label: "Mop Pad", life: { siid: 18, piid: 1 }, reset: { siid: 18, aiid: 1 } },
1770
- { key: "silver-ion", label: "Silver-ion", life: { siid: 19, piid: 2 }, reset: { siid: 19, aiid: 1 } },
1771
- { key: "detergent", label: "Detergent", life: { siid: 20, piid: 1 }, reset: { siid: 20, aiid: 1 } },
1804
+ {
1805
+ key: "silver-ion",
1806
+ label: "Silver-ion",
1807
+ life: { siid: 19, piid: 2 },
1808
+ reset: { siid: 19, aiid: 1 }
1809
+ },
1810
+ {
1811
+ key: "detergent",
1812
+ label: "Detergent",
1813
+ life: { siid: 20, piid: 1 },
1814
+ reset: { siid: 20, aiid: 1 }
1815
+ },
1772
1816
  { key: "squeegee", label: "Squeegee", life: { siid: 24, piid: 1 }, reset: { siid: 24, aiid: 1 } },
1773
- { key: "deodorizer", label: "Deodorizer", life: { siid: 29, piid: 2 }, reset: { siid: 29, aiid: 1 } },
1817
+ {
1818
+ key: "deodorizer",
1819
+ label: "Deodorizer",
1820
+ life: { siid: 29, piid: 2 },
1821
+ reset: { siid: 29, aiid: 1 }
1822
+ },
1774
1823
  { key: "wheel", label: "Wheel", life: { siid: 30, piid: 2 }, reset: { siid: 30, aiid: 1 } },
1775
- { key: "scale-inhibitor", label: "Scale Inhibitor", life: { siid: 31, piid: 2 }, reset: { siid: 31, aiid: 1 } },
1824
+ {
1825
+ key: "scale-inhibitor",
1826
+ label: "Scale Inhibitor",
1827
+ life: { siid: 31, piid: 2 },
1828
+ reset: { siid: 31, aiid: 1 }
1829
+ },
1776
1830
  { key: "dust-bag", label: "Dust Bag", life: { siid: 27, piid: 17 }, reset: null }
1777
1831
  ];
1778
1832
  function consumableSpec(key2) {
@@ -3887,6 +3941,9 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice {
3887
3941
 
3888
3942
  // src/models/mower/properties.ts
3889
3943
  var MOWER_PROP = {
3944
+ /** VERIFIED-by-donor — 20-byte heartbeat (sentinel 0xCE at [0]/[19]); byte 11
3945
+ * battery, byte 12 main-state, byte 13 task sub-state. See parseHeartbeat. */
3946
+ HEARTBEAT: { siid: 1, piid: 1 },
3890
3947
  /** VERIFIED-by-donor — pose + coverage telemetry (binary). P4 uses progress % only. */
3891
3948
  POSE_COVERAGE: { siid: 1, piid: 4 },
3892
3949
  /** VERIFIED-by-donor — MowerStatus (DeviceStatus). */
@@ -4236,6 +4293,39 @@ function parseMowerConsumables(result) {
4236
4293
  };
4237
4294
  });
4238
4295
  }
4296
+ var MOWER_TASK_SUBSTATES = [
4297
+ "idle",
4298
+ "starting",
4299
+ "mowing",
4300
+ "paused",
4301
+ "finished",
4302
+ "failed",
4303
+ "exit",
4304
+ "returning-to-dock"
4305
+ ];
4306
+ var HEARTBEAT_SENTINEL = 206;
4307
+ var HEARTBEAT_SUBSTATE_BASE = 33;
4308
+ var HEARTBEAT_MAIN_STATE_MOWING = 4;
4309
+ function parseMowerHeartbeat(value) {
4310
+ if (!Array.isArray(value) || value.length < 14) {
4311
+ return null;
4312
+ }
4313
+ const bytes = [];
4314
+ for (const b of value) {
4315
+ if (typeof b !== "number") {
4316
+ return null;
4317
+ }
4318
+ bytes.push(b);
4319
+ }
4320
+ if (bytes[0] !== HEARTBEAT_SENTINEL || bytes[bytes.length - 1] !== HEARTBEAT_SENTINEL) {
4321
+ return null;
4322
+ }
4323
+ const rawBattery = bytes[11] ?? 0;
4324
+ const mainState = ((bytes[12] ?? 0) & 15) - 1;
4325
+ const subStateRaw = bytes[13] ?? 0;
4326
+ const taskSubState = mainState === HEARTBEAT_MAIN_STATE_MOWING ? MOWER_TASK_SUBSTATES[subStateRaw - HEARTBEAT_SUBSTATE_BASE] ?? null : null;
4327
+ return { rawBattery, mainState, subStateRaw, taskSubState };
4328
+ }
4239
4329
 
4240
4330
  // src/models/mower/capabilities.ts
4241
4331
  var FALLBACK2 = {
@@ -4909,6 +4999,19 @@ var MowerDevice = class _MowerDevice extends BaseDevice {
4909
4999
  get fault() {
4910
5000
  return FAULT(this.faultRaw);
4911
5001
  }
5002
+ /**
5003
+ * Live mowing task sub-state decoded from the HEARTBEAT (1:1) blob —
5004
+ * idle/starting/mowing/paused/finished/failed/exit/returning-to-dock — or null
5005
+ * when no mowing session is in progress (or the heartbeat is absent). This is
5006
+ * the donor's "current task" sensor; the heartbeat is push-only (the mower does
5007
+ * not answer live reads), so it tracks the device while it is actively mowing.
5008
+ */
5009
+ get taskSubState() {
5010
+ const hb = parseMowerHeartbeat(
5011
+ this.getProperty(MOWER_PROP.HEARTBEAT.siid, MOWER_PROP.HEARTBEAT.piid)?.value
5012
+ );
5013
+ return hb?.taskSubState ?? null;
5014
+ }
4912
5015
  /** Parsed scheduling task descriptor (2:50), or null. */
4913
5016
  get task() {
4914
5017
  return parseTaskDescriptor(
@@ -5100,6 +5203,7 @@ var MowerDevice = class _MowerDevice extends BaseDevice {
5100
5203
  }
5101
5204
  /** Props worth seeding on start() / polling — exported for the facade. */
5102
5205
  static DEFAULT_PROPS = [
5206
+ MOWER_PROP.HEARTBEAT,
5103
5207
  MOWER_PROP.STATUS,
5104
5208
  MOWER_PROP.DEVICE_CODE,
5105
5209
  MOWER_PROP.BATTERY,
@@ -5728,6 +5832,7 @@ function createClientDumper(client, options) {
5728
5832
  DreameTransportError,
5729
5833
  LIBRARY_NAME,
5730
5834
  MOWER_MODEL_CAPABILITIES,
5835
+ MOWER_TASK_SUBSTATES,
5731
5836
  MiotError,
5732
5837
  MiotState,
5733
5838
  MowerCapabilityResolver,
@@ -5759,6 +5864,7 @@ function createClientDumper(client, options) {
5759
5864
  isDreameConsumableKey,
5760
5865
  mowerConsumableIndex,
5761
5866
  parseMowerConsumables,
5867
+ parseMowerHeartbeat,
5762
5868
  renderMowerSvg,
5763
5869
  renderVacuumPng,
5764
5870
  resolveCapabilities,