@mega-yfue/eufy-sdk 0.5.0-beta.51 → 0.5.0-beta.53

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.
@@ -454,8 +454,9 @@ export declare class EufyMega extends EventEmitter {
454
454
  *
455
455
  * Side-effect: registers `eufy_home_tuya` devices with the Tuya command router so
456
456
  * the command dispatcher can resolve a eufy SN → Tuya devId without a separate lookup.
457
- * The Tuya id is extracted from the device's raw cloud record (`tuya_uuid`, `tuya_virtual_id`,
458
- * `tuya_device_id`, or `virtualId` fields — whichever is non-empty).
457
+ * The Tuya id is the first non-empty `tuya_uuid`, `tuya_virtual_id`, `tuya_device_id` or
458
+ * `virtualId` on the raw cloud record, else the device's serial: the app keys its Tuya device by
459
+ * the eufy device id, and a record that carries none of those fields is read under its serial.
459
460
  *
460
461
  * A partial cloud outage still resolves, with the devices that answered plus the ones already known — but a
461
462
  * session the cloud has rejected REJECTS, with {@link SessionExpiredError}. An empty list would be
@@ -790,6 +791,10 @@ export declare class EufyMega extends EventEmitter {
790
791
  /**
791
792
  * Subscribe the devices on one credential scope; a failing subscribe is reported, not fatal (one
792
793
  * unreachable device must not stop the rest of the roster from coming up).
794
+ *
795
+ * A Tuya clean-line device's DPs are also read from the Tuya cloud, whether or not its subscribe
796
+ * succeeded: the cloud read is a separate plane, and the broker can refuse a subscribe for a device
797
+ * whose DPs the Tuya cloud still serves.
793
798
  */
794
799
  private subscribeMqttDevices;
795
800
  /**
package/dist/index.js CHANGED
@@ -7769,6 +7769,12 @@ var ARMING_CMD = {
7769
7769
  SET_ALL_ACTION: 1255
7770
7770
  };
7771
7771
  var DEVICE_NOTIFY_FLAG = 8;
7772
+ var STANDALONE_ACTION_MODELS = /* @__PURE__ */ new Set(["T8170", "T8171"]);
7773
+ var STANDALONE_READ_MODELS = /* @__PURE__ */ new Set([...STANDALONE_ACTION_MODELS, "T8400", "T8442", "T8410"]);
7774
+ function ownAction(read, mode) {
7775
+ const value = Number(read(`${mode}Action`)?.value);
7776
+ return Number.isInteger(value) && value >= 0 ? value : void 0;
7777
+ }
7772
7778
  function actionTable(read, mode) {
7773
7779
  const value = read(`${mode}ActionTable`)?.value;
7774
7780
  return value && typeof value === "object" && Array.isArray(value.devices) ? value : void 0;
@@ -7811,6 +7817,13 @@ function alarmDelayCommand(mode, config, ctx) {
7811
7817
  };
7812
7818
  return setJsonRaw(ARMING_CMD.SET_ALL_ACTION, data, ctx, STATION_CHANNEL2);
7813
7819
  }
7820
+ function ownTable(read, mode, channel) {
7821
+ const flags = ownAction(read, mode);
7822
+ if (flags === void 0)
7823
+ return void 0;
7824
+ const wire = ARMING_MODE_WIRE[mode];
7825
+ return { mode_id: wire, devices: [{ device_channel: channel, action: wire << 24 | flags }] };
7826
+ }
7814
7827
  var ACTION_TABLE_LAG_MS = 15e3;
7815
7828
  function withDeviceNotification(table, channel, on) {
7816
7829
  if (!table?.devices.some((d) => d.device_channel === channel))
@@ -7879,25 +7892,29 @@ var ARMING_MEMBERS = {
7879
7892
  /**
7880
7893
  * Whether the device on `channel` sends a push notification in `mode`: its {@link DEVICE_NOTIFY_FLAG}
7881
7894
  * in the action table the station reports for that mode. `undefined` where the station reports no table
7882
- * or the table does not list `channel`. A device's channel is `EufyDevice.channel`.
7895
+ * or the table does not list `channel`. A standalone camera answers from its own 1225/1239/1148 flags,
7896
+ * for its own channel only. A device's channel is `EufyDevice.channel`.
7883
7897
  */
7884
- deviceNotification: method(({ read }) => (mode, channel) => {
7885
- const action = actionTable(read, mode)?.devices.find((d) => d.device_channel === channel)?.action;
7898
+ deviceNotification: method(({ ctx, read }) => (mode, channel) => {
7899
+ const action = ctx.model === "T8010" ? actionTable(read, mode)?.devices.find((d) => d.device_channel === channel)?.action : channel === ctx.channel ? ownAction(read, mode) : void 0;
7886
7900
  return typeof action === "number" ? (action & DEVICE_NOTIFY_FLAG) !== 0 : void 0;
7887
- }, "Whether the device on a channel sends a push notification in a guard mode (home/away).", (ctx) => ctx.model === "T8010"),
7901
+ }, "Whether the device on a channel sends a push notification in a guard mode (home/away/custom1).", (ctx) => ctx.model === "T8010" || STANDALONE_READ_MODELS.has(ctx.model ?? "")),
7888
7902
  /**
7889
7903
  * Turn the push notification of the device on `channel` on or off in `mode` (cmd
7890
7904
  * {@link ARMING_CMD.SET_ALL_ACTION}): the station's reported table written back with that one flag
7891
- * changed. Rejects, sending nothing, where the table does not list `channel`. A write builds on the last
7892
- * one sent for that mode until the station reports a table observed 15 s after it, so a second write does
7893
- * not revert it; a write whose dispatch rejects is not built on.
7905
+ * changed. A standalone camera is sent a one-entry `{mode_id, devices}` table built from its own flags,
7906
+ * for its own channel only. Rejects, sending nothing, where the table does not list `channel`. A write
7907
+ * builds on the last one sent for that mode until the device reports a table (`${mode}Action` on a
7908
+ * standalone camera) observed 15 s after it, so a second write does not revert it; a write whose dispatch
7909
+ * rejects is not built on.
7894
7910
  */
7895
7911
  setDeviceNotification: method(({ ctx, sink, read }) => {
7896
7912
  const sent2 = /* @__PURE__ */ new Map();
7897
7913
  return (mode, channel, on) => {
7898
7914
  const last = sent2.get(mode);
7899
- const observed = read(`${mode}ActionTable`)?.ts ?? 0;
7900
- const base = last && observed < last.at + ACTION_TABLE_LAG_MS ? last.table : actionTable(read, mode);
7915
+ const reported = ctx.model === "T8010" ? `${mode}ActionTable` : `${mode}Action`;
7916
+ const observed = read(reported)?.ts ?? 0;
7917
+ const base = last && observed < last.at + ACTION_TABLE_LAG_MS ? last.table : ctx.model === "T8010" ? actionTable(read, mode) : ownTable(read, mode, ctx.channel);
7901
7918
  const table = withDeviceNotification(base, channel, on);
7902
7919
  if (!table) {
7903
7920
  return Promise.reject(new Error(`arming: no ${mode} action table lists channel ${channel} [${describeDevice(ctx)}]`));
@@ -7913,7 +7930,7 @@ var ARMING_MEMBERS = {
7913
7930
  throw err;
7914
7931
  });
7915
7932
  };
7916
- }, "Turn the push notification of the device on a channel on or off in a guard mode (home/away).", (ctx) => ctx.model === "T8010")
7933
+ }, "Turn the push notification of the device on a channel on or off in a guard mode (home/away/custom1).", (ctx) => ctx.model === "T8010" || STANDALONE_ACTION_MODELS.has(ctx.model ?? ""))
7917
7934
  };
7918
7935
  function decodeAlarmPhase(signal) {
7919
7936
  const type = signal.source === "push" ? signal.payload?.["type"] : void 0;
@@ -11544,7 +11561,7 @@ var SECURITY_PARAMS = {
11544
11561
  1148: {
11545
11562
  name: "custom1Action",
11546
11563
  type: "number",
11547
- provenance: "apk"
11564
+ provenance: "verified"
11548
11565
  },
11549
11566
  1149: {
11550
11567
  name: "custom2Action",
@@ -11759,7 +11776,7 @@ var SECURITY_PARAMS = {
11759
11776
  1225: {
11760
11777
  name: "homeAction",
11761
11778
  type: "number",
11762
- provenance: "apk"
11779
+ provenance: "verified"
11763
11780
  },
11764
11781
  1229: {
11765
11782
  name: "param1229",
@@ -25525,11 +25542,11 @@ function buildPublishDpsAction(devId, gwId, dps) {
25525
25542
  postData: JSON.stringify({ gwId, devId, dps: JSON.stringify(dps) })
25526
25543
  };
25527
25544
  }
25528
- function buildGetDeviceDpsAction(devId, dpCacheType = 1) {
25545
+ function buildGetDeviceDpsAction(devId) {
25529
25546
  return {
25530
- a: "smartlife.m.device.cache.dp.get",
25531
- v: "2.0",
25532
- postData: JSON.stringify({ devId, dpCacheType })
25547
+ a: "smartlife.m.device.dp.get",
25548
+ v: "1.0",
25549
+ postData: JSON.stringify({ devId })
25533
25550
  };
25534
25551
  }
25535
25552
  function buildUsernameTokenGetAction(countryCode, username) {
@@ -25685,11 +25702,12 @@ var TuyaClient = class {
25685
25702
  return { sid: result.sid, uid: result.uid };
25686
25703
  }
25687
25704
  /**
25688
- * READ/dump a device's cached data-points (`smartlife.m.device.cache.dp.get`).
25689
- * Builds the request without needing a working signer; the signer is only exercised on send.
25705
+ * READ a device's current data-points (`smartlife.m.device.dp.get`). The envelope's `result` is a
25706
+ * flat `{ "<dpId>": value }` map. Builds the request without needing a working signer; the signer is
25707
+ * only exercised on send.
25690
25708
  */
25691
- async getDeviceDps(devId, dpCacheType) {
25692
- return this.call(buildGetDeviceDpsAction(devId, dpCacheType));
25709
+ async getDeviceDps(devId) {
25710
+ return this.call(buildGetDeviceDpsAction(devId));
25693
25711
  }
25694
25712
  /**
25695
25713
  * CONTROL: publish data-points to a device (`smartlife.m.device.dp.publish`). `dps` is
@@ -25780,9 +25798,9 @@ var TuyaCommandRouter = class {
25780
25798
  /** Promise that resolves once Tuya login has completed. Reset by {@link bind}. */
25781
25799
  loginOnce = null;
25782
25800
  /**
25783
- * eufy SN → Tuya device ids, populated by the facade via {@link registerDevice}.
25784
- * The facade extracts the Tuya id from the `raw` device record fields (`tuya_uuid`,
25785
- * `tuya_virtual_id`, `tuya_device_id`, `virtualId`) and registers it once the device list loads.
25801
+ * eufy SN → Tuya device ids, populated via {@link registerDevice} once the device list loads.
25802
+ * The Tuya id is the record's first non-empty `tuya_uuid` / `tuya_virtual_id` / `tuya_device_id` /
25803
+ * `virtualId`, or the device's serial when the record carries none of them.
25786
25804
  */
25787
25805
  snMap = /* @__PURE__ */ new Map();
25788
25806
  constructor(config = {}) {
@@ -25804,10 +25822,10 @@ var TuyaCommandRouter = class {
25804
25822
  this.loginOnce = null;
25805
25823
  }
25806
25824
  /**
25807
- * Register a eufy SN → Tuya devId mapping. Called by the facade for each `eufy_home_tuya`
25808
- * device after the cloud device list loads. The facade extracts the Tuya id from the device's
25809
- * raw record (`tuya_uuid` / `tuya_virtual_id` / `tuya_device_id` / `virtualId` fields).
25810
- * `gwId` defaults to `devId` — standalone devices share the two.
25825
+ * Register a eufy SN → Tuya devId mapping for an `eufy_home_tuya` device. `devId` is the record's
25826
+ * first non-empty `tuya_uuid` / `tuya_virtual_id` / `tuya_device_id` / `virtualId`, or the device's
25827
+ * serial when the record carries none of them. `gwId` defaults to `devId` — standalone devices
25828
+ * share the two.
25811
25829
  */
25812
25830
  registerDevice(sn, devId, gwId = devId) {
25813
25831
  this.snMap.set(sn, { devId, gwId });
@@ -25831,13 +25849,9 @@ var TuyaCommandRouter = class {
25831
25849
  return this.loginOnce;
25832
25850
  }
25833
25851
  /**
25834
- * Fetch a device's cached DPs from the Tuya cloud (`smartlife.m.device.cache.dp.get`) and deliver
25835
- * the raw DP map to the caller. Used for initial state hydration after MQTT subscribe — gets the
25836
- * last-known state without waiting for the first realtime push.
25837
- *
25838
- * The response shape from `getDeviceDps` is not yet pinned from a live capture. The defensive
25839
- * extraction tries both `result.dps` (a nested map) and bare `result` (a flat map), and returns
25840
- * `null` when neither yields a non-empty record.
25852
+ * Read a device's current DPs from the Tuya cloud (`smartlife.m.device.dp.get`) and answer the DP
25853
+ * map: the reply's flat `result`, kept to integer DP keys with scalar values. Answers `null` for an
25854
+ * unregistered device, a failed reply, or a reply with no DPs.
25841
25855
  */
25842
25856
  async fetchDps(sn) {
25843
25857
  const ids = this.snMap.get(sn);
@@ -25847,12 +25861,8 @@ var TuyaCommandRouter = class {
25847
25861
  const res = await this.client.getDeviceDps(ids.devId);
25848
25862
  if (!res.success || !res.result)
25849
25863
  return null;
25850
- const nested = res.result.dps;
25851
- if (nested && typeof nested === "object" && !Array.isArray(nested) && Object.keys(nested).length > 0) {
25852
- return nested;
25853
- }
25854
- const flat = parseTuyaDpEvent(res.result);
25855
- return flat ? flat : null;
25864
+ const dps = parseTuyaDpEvent(res.result);
25865
+ return dps ? dps : null;
25856
25866
  }
25857
25867
  /**
25858
25868
  * Route an `aiot-dp` {@link Command} to the Tuya REST API.
@@ -25871,7 +25881,7 @@ var TuyaCommandRouter = class {
25871
25881
  await this.ensureLoggedIn();
25872
25882
  const ids = this.snMap.get(sn);
25873
25883
  if (!ids) {
25874
- throw new Error(`TuyaCommandRouter: no Tuya device id registered for ${sn}. Ensure getDevices() was called and the cloud record includes a tuya_uuid / tuya_virtual_id field.`);
25884
+ throw new Error(`TuyaCommandRouter: no Tuya device id registered for ${sn}. Ensure getDevices() was called; it registers every eufy_home_tuya device under its Tuya id or its serial.`);
25875
25885
  }
25876
25886
  await this.client.publishDps(ids.devId, ids.gwId, { [cmd.dp]: cmd.value }, { allowUnverified: this.allowUnverified });
25877
25887
  }
@@ -28679,8 +28689,9 @@ var EufyMega = class extends EventEmitter13 {
28679
28689
  *
28680
28690
  * Side-effect: registers `eufy_home_tuya` devices with the Tuya command router so
28681
28691
  * the command dispatcher can resolve a eufy SN → Tuya devId without a separate lookup.
28682
- * The Tuya id is extracted from the device's raw cloud record (`tuya_uuid`, `tuya_virtual_id`,
28683
- * `tuya_device_id`, or `virtualId` fields — whichever is non-empty).
28692
+ * The Tuya id is the first non-empty `tuya_uuid`, `tuya_virtual_id`, `tuya_device_id` or
28693
+ * `virtualId` on the raw cloud record, else the device's serial: the app keys its Tuya device by
28694
+ * the eufy device id, and a record that carries none of those fields is read under its serial.
28684
28695
  *
28685
28696
  * A partial cloud outage still resolves, with the devices that answered plus the ones already known — but a
28686
28697
  * session the cloud has rejected REJECTS, with {@link SessionExpiredError}. An empty list would be
@@ -28692,9 +28703,7 @@ var EufyMega = class extends EventEmitter13 {
28692
28703
  if (dev.category !== "eufy_home_tuya")
28693
28704
  continue;
28694
28705
  const raw = dev.raw ?? {};
28695
- const devId = tuyaDevIdFrom(raw);
28696
- if (devId)
28697
- this.tuya.registerDevice(dev.sn, devId);
28706
+ this.tuya.registerDevice(dev.sn, tuyaDevIdFrom(raw) ?? dev.sn);
28698
28707
  }
28699
28708
  return devices;
28700
28709
  }
@@ -29362,21 +29371,25 @@ var EufyMega = class extends EventEmitter13 {
29362
29371
  /**
29363
29372
  * Subscribe the devices on one credential scope; a failing subscribe is reported, not fatal (one
29364
29373
  * unreachable device must not stop the rest of the roster from coming up).
29374
+ *
29375
+ * A Tuya clean-line device's DPs are also read from the Tuya cloud, whether or not its subscribe
29376
+ * succeeded: the cloud read is a separate plane, and the broker can refuse a subscribe for a device
29377
+ * whose DPs the Tuya cloud still serves.
29365
29378
  */
29366
29379
  async subscribeMqttDevices(scope, transport) {
29367
29380
  for (const d of this.getMqttDevices().filter((dev) => mqttScopeFor(dev) === scope)) {
29368
29381
  try {
29369
29382
  await transport.subscribeDevice(d);
29370
29383
  await this.sendRealtimeInit(d.sn);
29371
- if (d.category === "eufy_home_tuya") {
29372
- this.tuya.fetchDps(d.sn).then((dps) => {
29373
- if (dps)
29374
- this.tuyaDpRouter.deliver(d.sn, dps);
29375
- }).catch((e) => this.reportError(e));
29376
- }
29377
29384
  } catch (e) {
29378
29385
  this.reportError(e);
29379
29386
  }
29387
+ if (d.category === "eufy_home_tuya") {
29388
+ this.tuya.fetchDps(d.sn).then((dps) => {
29389
+ if (dps)
29390
+ this.tuyaDpRouter.deliver(d.sn, dps);
29391
+ }).catch((e) => this.reportError(e));
29392
+ }
29380
29393
  }
29381
29394
  }
29382
29395
  /**