@apocaliss92/nodedreame 1.10.0 → 1.11.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
@@ -69,9 +69,12 @@ __export(index_exports, {
69
69
  decodeAutoSwitchAll: () => decodeAutoSwitchAll,
70
70
  encodeAiFeatureWrite: () => encodeAiFeatureWrite,
71
71
  encodeAutoSwitchWrite: () => encodeAutoSwitchWrite,
72
+ extractMowerConsumableValues: () => extractMowerConsumableValues,
72
73
  getMowerCapabilities: () => getMowerCapabilities,
73
74
  getVacuumCapabilities: () => getVacuumCapabilities,
74
75
  isDreameConsumableKey: () => isDreameConsumableKey,
76
+ mowerConsumableIndex: () => mowerConsumableIndex,
77
+ parseMowerConsumables: () => parseMowerConsumables,
75
78
  renderMowerSvg: () => renderMowerSvg,
76
79
  renderVacuumPng: () => renderVacuumPng,
77
80
  resolveCapabilities: () => resolveCapabilities,
@@ -81,7 +84,7 @@ module.exports = __toCommonJS(index_exports);
81
84
 
82
85
  // src/support/version.ts
83
86
  var LIBRARY_NAME = "nodedreame";
84
- var LIBRARY_VERSION = "1.10.0";
87
+ var LIBRARY_VERSION = "1.11.1";
85
88
 
86
89
  // src/transport/errors.ts
87
90
  var DreameError = class extends Error {
@@ -697,6 +700,7 @@ var DreamePush = class extends TypedEmitter {
697
700
  #region;
698
701
  #connect;
699
702
  #backoffMs;
703
+ #keepaliveSeconds;
700
704
  #rejectUnauthorized;
701
705
  #topic;
702
706
  #client = null;
@@ -717,6 +721,7 @@ var DreamePush = class extends TypedEmitter {
717
721
  this.#region = input.region;
718
722
  this.#connect = input.connect ?? defaultConnect;
719
723
  this.#backoffMs = input.reconnectBackoffMs ?? 5e3;
724
+ this.#keepaliveSeconds = input.keepaliveSeconds ?? 60;
720
725
  this.#rejectUnauthorized = input.rejectUnauthorized ?? false;
721
726
  this.#topic = buildStatusTopic(this.#device, this.#session.uid, this.#region);
722
727
  this.#onClose = () => {
@@ -774,6 +779,7 @@ var DreamePush = class extends TypedEmitter {
774
779
  reconnectPeriod: 0,
775
780
  // we drive reconnect ourselves
776
781
  connectTimeout: 15e3,
782
+ keepalive: this.#keepaliveSeconds,
777
783
  rejectUnauthorized: this.#rejectUnauthorized,
778
784
  clean: true
779
785
  });
@@ -781,25 +787,37 @@ var DreamePush = class extends TypedEmitter {
781
787
  client.on("message", (_topic, payload) => this.#handleMessage(payload));
782
788
  client.on("error", (err) => this.emit("error", err));
783
789
  client.on("close", this.#onClose);
784
- await new Promise((resolve, reject) => {
785
- const onConnect = () => {
786
- client.removeListener("error", onError);
787
- client.subscribe(this.#topic, { qos: 0 }, (err) => {
788
- if (err) {
789
- reject(new DreameTransportError(`mqtt subscribe failed: ${err.message}`, err));
790
- return;
791
- }
792
- this.emit("connect");
793
- resolve();
794
- });
795
- };
796
- const onError = (err) => {
797
- client.removeListener("connect", onConnect);
798
- reject(new DreameTransportError(`mqtt connect failed: ${err.message}`, err));
799
- };
800
- client.once("connect", onConnect);
801
- client.once("error", onError);
802
- });
790
+ try {
791
+ await new Promise((resolve, reject) => {
792
+ const onConnect = () => {
793
+ client.removeListener("error", onError);
794
+ client.subscribe(this.#topic, { qos: 0 }, (err) => {
795
+ if (err) {
796
+ reject(new DreameTransportError(`mqtt subscribe failed: ${err.message}`, err));
797
+ return;
798
+ }
799
+ this.emit("connect");
800
+ resolve();
801
+ });
802
+ };
803
+ const onError = (err) => {
804
+ client.removeListener("connect", onConnect);
805
+ reject(new DreameTransportError(`mqtt connect failed: ${err.message}`, err));
806
+ };
807
+ client.once("connect", onConnect);
808
+ client.once("error", onError);
809
+ });
810
+ } catch (err) {
811
+ client.removeListener("close", this.#onClose);
812
+ try {
813
+ client.end(true, {}, () => void 0);
814
+ } catch {
815
+ }
816
+ if (this.#client === client) {
817
+ this.#client = null;
818
+ }
819
+ throw err;
820
+ }
803
821
  }
804
822
  #scheduleReconnect() {
805
823
  if (this.#closed || this.#reconnectTimer) {
@@ -3931,6 +3949,19 @@ function buildEdgePayload(contourIds) {
3931
3949
  function buildSpotPayload(spotAreaIds) {
3932
3950
  return { m: "a", p: 0, o: TASK_OPCODE.SPOT, d: { area: [...spotAreaIds] } };
3933
3951
  }
3952
+ function buildGetConsumablePayload() {
3953
+ return { m: "g", t: "CMS" };
3954
+ }
3955
+ function buildSetConsumablePayload(values) {
3956
+ const normalized = values.map((v) => Math.trunc(v));
3957
+ if (normalized.length !== 3) {
3958
+ throw new RangeError(`CMS values must contain exactly 3 counters; got ${normalized.length}`);
3959
+ }
3960
+ if (normalized.some((v) => v < 0)) {
3961
+ throw new RangeError(`CMS values cannot be negative; got ${JSON.stringify(normalized)}`);
3962
+ }
3963
+ return { m: "s", t: "CMS", d: { value: normalized } };
3964
+ }
3934
3965
 
3935
3966
  // src/models/mower/enums.ts
3936
3967
  var MowerStatus = /* @__PURE__ */ ((MowerStatus2) => {
@@ -4122,6 +4153,104 @@ function parseControlStatus(value) {
4122
4153
  zones
4123
4154
  };
4124
4155
  }
4156
+ var MOWER_CONSUMABLES = [
4157
+ { key: "blade", index: 0, totalMinutes: 6e3 },
4158
+ { key: "brush", index: 1, totalMinutes: 3e4 },
4159
+ { key: "maintenance", index: 2, totalMinutes: 3600 }
4160
+ ];
4161
+ function mowerConsumableIndex(item) {
4162
+ switch (item.trim().toLowerCase()) {
4163
+ case "blade":
4164
+ case "blades":
4165
+ return 0;
4166
+ case "brush":
4167
+ case "cleaning_brush":
4168
+ return 1;
4169
+ case "robot":
4170
+ case "maintenance":
4171
+ case "robot_maintenance":
4172
+ return 2;
4173
+ default:
4174
+ return null;
4175
+ }
4176
+ }
4177
+ function toInt(v) {
4178
+ if (typeof v === "number" && Number.isFinite(v)) {
4179
+ return Math.trunc(v);
4180
+ }
4181
+ if (typeof v === "string") {
4182
+ const n = Number(v);
4183
+ return Number.isFinite(n) ? Math.trunc(n) : null;
4184
+ }
4185
+ return null;
4186
+ }
4187
+ function extractCustomActionData(result) {
4188
+ if (!isRecord2(result)) {
4189
+ return null;
4190
+ }
4191
+ if (Array.isArray(result["value"])) {
4192
+ return result;
4193
+ }
4194
+ if (isRecord2(result["d"])) {
4195
+ return result["d"];
4196
+ }
4197
+ const out = result["out"];
4198
+ if (!Array.isArray(out)) {
4199
+ return null;
4200
+ }
4201
+ for (const entry of out) {
4202
+ if (!isRecord2(entry)) {
4203
+ continue;
4204
+ }
4205
+ const r = entry["r"];
4206
+ const code = entry["code"];
4207
+ const rError = r !== void 0 && r !== null && r !== 0;
4208
+ const codeError = code !== void 0 && code !== null && code !== 0;
4209
+ if (rError && codeError) {
4210
+ continue;
4211
+ }
4212
+ if (isRecord2(entry["d"])) {
4213
+ return entry["d"];
4214
+ }
4215
+ }
4216
+ return null;
4217
+ }
4218
+ function extractMowerConsumableValues(result) {
4219
+ const data = extractCustomActionData(result);
4220
+ if (data === null) {
4221
+ return null;
4222
+ }
4223
+ const values = data["value"];
4224
+ if (!Array.isArray(values) || values.length < 3) {
4225
+ return null;
4226
+ }
4227
+ const out = [];
4228
+ for (const v of values.slice(0, 3)) {
4229
+ const n = toInt(v);
4230
+ if (n === null) {
4231
+ return null;
4232
+ }
4233
+ out.push(n);
4234
+ }
4235
+ return out;
4236
+ }
4237
+ function parseMowerConsumables(result) {
4238
+ const values = extractMowerConsumableValues(result);
4239
+ if (values === null) {
4240
+ return null;
4241
+ }
4242
+ return MOWER_CONSUMABLES.map((c) => {
4243
+ const used = values[c.index] ?? 0;
4244
+ const remaining = c.totalMinutes - used;
4245
+ const pct = Math.max(0, Math.min(100, Math.round(remaining / c.totalMinutes * 1e3) / 10));
4246
+ return {
4247
+ key: c.key,
4248
+ usedMinutes: used,
4249
+ totalMinutes: c.totalMinutes,
4250
+ remainingPercent: pct
4251
+ };
4252
+ });
4253
+ }
4125
4254
 
4126
4255
  // src/models/mower/capabilities.ts
4127
4256
  var FALLBACK2 = {
@@ -4887,6 +5016,56 @@ var MowerDevice = class _MowerDevice extends BaseDevice {
4887
5016
  }
4888
5017
  return this.#sendTask(buildSpotPayload(spotAreaIds.map((s) => Math.trunc(s))));
4889
5018
  }
5019
+ // -- CMS consumables ----------------------------------------------------
5020
+ /**
5021
+ * Read the raw CMS consumable counters `[blade, brush, robot]` (minutes used),
5022
+ * via the SCHEDULING_TASK (2:50) custom-action getter `{m:'g',t:'CMS'}`. This
5023
+ * is a LIVE device action (it wakes the mower); rejects with
5024
+ * {@link DreameDeviceOfflineError} when the mower is unreachable. Returns null
5025
+ * if the response is malformed.
5026
+ */
5027
+ async getConsumableValues() {
5028
+ const result = await this.callAction(
5029
+ MOWER_PROP.SCHEDULING_TASK.siid,
5030
+ MOWER_PROP.SCHEDULING_TASK.piid,
5031
+ [buildGetConsumablePayload()]
5032
+ );
5033
+ return extractMowerConsumableValues(result);
5034
+ }
5035
+ /**
5036
+ * Read the CMS consumables as typed readings (blade/brush/maintenance) with
5037
+ * remaining %. LIVE action — see {@link getConsumableValues}. Returns null on
5038
+ * a malformed response.
5039
+ */
5040
+ async getConsumables() {
5041
+ const result = await this.callAction(
5042
+ MOWER_PROP.SCHEDULING_TASK.siid,
5043
+ MOWER_PROP.SCHEDULING_TASK.piid,
5044
+ [buildGetConsumablePayload()]
5045
+ );
5046
+ return parseMowerConsumables(result);
5047
+ }
5048
+ /**
5049
+ * Reset one CMS consumable counter to zero. Reads the current counters, zeroes
5050
+ * the selected one (leaving the others), and writes them back via the
5051
+ * `{m:'s',t:'CMS',d:{value:[…]}}` setter. LIVE action. Throws on an unknown
5052
+ * item or when the current counters cannot be read.
5053
+ */
5054
+ async resetConsumable(item) {
5055
+ const index = mowerConsumableIndex(item);
5056
+ if (index === null) {
5057
+ throw new DreameError(`resetConsumable: unknown consumable item "${item}"`);
5058
+ }
5059
+ const current = await this.getConsumableValues();
5060
+ if (current === null) {
5061
+ throw new DreameError("resetConsumable: failed to read current CMS counters");
5062
+ }
5063
+ const next = [...current];
5064
+ next[index] = 0;
5065
+ await this.callAction(MOWER_PROP.SCHEDULING_TASK.siid, MOWER_PROP.SCHEDULING_TASK.piid, [
5066
+ buildSetConsumablePayload(next)
5067
+ ]);
5068
+ }
4890
5069
  /**
4891
5070
  * Seed the cache from the CLOUD SHADOW (last-known values) WITHOUT waking the
4892
5071
  * mower — reads {@link MowerDevice.DEFAULT_PROPS} from the cloud-cached
@@ -5589,9 +5768,12 @@ function createClientDumper(client, options) {
5589
5768
  decodeAutoSwitchAll,
5590
5769
  encodeAiFeatureWrite,
5591
5770
  encodeAutoSwitchWrite,
5771
+ extractMowerConsumableValues,
5592
5772
  getMowerCapabilities,
5593
5773
  getVacuumCapabilities,
5594
5774
  isDreameConsumableKey,
5775
+ mowerConsumableIndex,
5776
+ parseMowerConsumables,
5595
5777
  renderMowerSvg,
5596
5778
  renderVacuumPng,
5597
5779
  resolveCapabilities,