@mega-yfue/eufy-sdk 0.5.0-beta.14 → 0.5.0-beta.16

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.
@@ -16,7 +16,10 @@ export interface DeviceRecord {
16
16
  deviceType?: number;
17
17
  model?: string;
18
18
  category?: string;
19
- /** The app-shown device name (`device_name`); see {@link CloudRecord.name}. */
19
+ /**
20
+ * The app-shown device name: `alias_name` first on a vacuum, `device_name` first otherwise;
21
+ * see {@link CloudRecord.name}.
22
+ */
20
23
  name?: string;
21
24
  /** Parent HomeBase serial when attached (topology signal; see {@link CloudRecord.parentSn}). */
22
25
  parentSn?: string;
@@ -92,6 +95,8 @@ export declare class DeviceRegistry {
92
95
  private overlayRefusalReported;
93
96
  /** Per-serial capability cache for {@link capabilitiesForDevice}; `null` = negative hit. */
94
97
  private readonly deviceCapsCache;
98
+ /** Per-serial cloud data points of an AIoT robot, joined beneath the params in {@link record} — see {@link loadCloudDps}. */
99
+ private readonly cloudDps;
95
100
  /** Per-serial realtime state, keyed by param id in the device's own namespace (see {@link DeviceRecord.dpParams}). */
96
101
  private readonly dpParams;
97
102
  /** Callers blocked in {@link awaitRealtimeState}, released by the device's first report. */
@@ -181,9 +186,26 @@ export declare class DeviceRegistry {
181
186
  * transport stacks own the claim predicate over the record.
182
187
  */
183
188
  require(sn: string): EufyDevice;
189
+ /**
190
+ * Hold an AIoT robot's cloud data points: the cloud's last-known value of each one it has reported,
191
+ * from `getDeviceRelationList`'s per-device `dps`.
192
+ *
193
+ * The device list carries only some of a robot's data points, so a point the robot publishes on change
194
+ * alone (its scene list, its schedules, its consumables) has no value until it next reports. The
195
+ * relation list holds every one, for an account the robot is shared with as well as its owner.
196
+ *
197
+ * Kept apart from the device list's params rather than merged into them: those are what a poll diffs,
198
+ * and a diff retires the realtime report for that id, so a cloud value lagging the robot's last report
199
+ * would replace it. {@link record} joins this beneath both halves. A failure keeps what was held.
200
+ *
201
+ * Limited to {@link CLOUD_DPS_MODELS}, the models whose relation-list `dps` are checked to carry the
202
+ * DP numbers their realtime reports use.
203
+ */
204
+ private loadCloudDps;
184
205
  /**
185
206
  * Resolve a serial to a {@link DeviceRecord} with current params: starts from the device-list params, then
186
- * overlays a fresh `get_device_param_list` when that call is available to this account.
207
+ * overlays a fresh `get_device_param_list` when that call is available to this account. An AIoT robot's
208
+ * cloud data points ({@link loadCloudDps}) sit beneath both, filling only ids neither carries.
187
209
  *
188
210
  * The overlay is **owner-gated** — a shared or member account is refused it for every device, permanently,
189
211
  * which {@link OWNER_ONLY_CODE} identifies — so when it is unavailable the device list is the source
package/dist/index.js CHANGED
@@ -1996,6 +1996,17 @@ ${this.auth_?.authToken ?? ""}`;
1996
1996
  device_sns: [deviceSn]
1997
1997
  });
1998
1998
  }
1999
+ /**
2000
+ * Fetch the account's devices as the `devicerelation` service lists them, `{attribute: 7, house_id: ""}`.
2001
+ *
2002
+ * Each entry is `{device: {device_sn, dps, …}}`, and `dps` is the cloud's last-known value of every
2003
+ * data point that device has reported, keyed by DP number. Answered for an account a device is shared
2004
+ * with as well as for its owner. Sent with `category: eufy_home`, without which the service answers
2005
+ * `10000`.
2006
+ */
2007
+ getDeviceRelationList() {
2008
+ return this.post("devicerelation", "/app/devicerelation/get_device_list", { attribute: 7, house_id: "" }, { category: "eufy_home" });
2009
+ }
1999
2010
  /**
2000
2011
  * Fetch one page of a device's **cleaning history** from the mega `clean` service.
2001
2012
  *
@@ -11046,6 +11057,8 @@ var MODEL_REGISTRY = {
11046
11057
  T8210: { codec: "camera", caps: ["doorbell", "battery"], name: "Video Doorbell" },
11047
11058
  // Confirmed against a real owned unit (named "Doorbell"): Video Doorbell Dual.
11048
11059
  T8214: { codec: "camera", caps: ["doorbell", "battery"], name: "Video Doorbell Dual" },
11060
+ // Battery Video Doorbell Dual, vendor deviceType 91 (BATTERY_DOORBELL_PLUS), reported by two users.
11061
+ T8213: { codec: "camera", caps: ["doorbell", "battery"], name: "Video Doorbell Dual" },
11049
11062
  // Confirmed against a real owned unit: the mains-powered Wired Doorbell 2K. No `battery` row
11050
11063
  // member on purpose — this model is wired, so the codec baseline plus inference is the whole
11051
11064
  // truth for power. Without this row it classified as a plain camera, so the doorbell capability
@@ -26330,7 +26343,14 @@ function deviceClassOf(codec, realtime) {
26330
26343
  const cls = CLASS_BY_CODEC[codec];
26331
26344
  return cls === "camera" && realtime !== "p2p" ? "other" : cls;
26332
26345
  }
26346
+ function recordName(raw, deviceClass) {
26347
+ if (deviceClass === "vacuum") {
26348
+ return raw.alias_name || raw.device_alias_name || raw.device_name || void 0;
26349
+ }
26350
+ return raw.device_name ?? raw.device_alias_name ?? raw.alias_name;
26351
+ }
26333
26352
  var LIST_REUSE_MS = 5e3;
26353
+ var CLOUD_DPS_MODELS = /* @__PURE__ */ new Set(["T2351"]);
26334
26354
  var DeviceRegistry = class {
26335
26355
  mega;
26336
26356
  onError;
@@ -26355,6 +26375,8 @@ var DeviceRegistry = class {
26355
26375
  overlayRefusalReported = false;
26356
26376
  /** Per-serial capability cache for {@link capabilitiesForDevice}; `null` = negative hit. */
26357
26377
  deviceCapsCache = /* @__PURE__ */ new Map();
26378
+ /** Per-serial cloud data points of an AIoT robot, joined beneath the params in {@link record} — see {@link loadCloudDps}. */
26379
+ cloudDps = /* @__PURE__ */ new Map();
26358
26380
  /** Per-serial realtime state, keyed by param id in the device's own namespace (see {@link DeviceRecord.dpParams}). */
26359
26381
  dpParams = /* @__PURE__ */ new Map();
26360
26382
  /** Callers blocked in {@link awaitRealtimeState}, released by the device's first report. */
@@ -26449,9 +26471,10 @@ var DeviceRegistry = class {
26449
26471
  category: raw.category,
26450
26472
  params
26451
26473
  }).codec;
26474
+ const deviceClass = deviceClassOf(codec, c.realtime);
26452
26475
  seen.set(raw.device_sn, {
26453
26476
  sn: raw.device_sn,
26454
- name: raw.device_name ?? raw.device_alias_name ?? raw.alias_name,
26477
+ name: recordName(raw, deviceClass),
26455
26478
  model: raw.device_model,
26456
26479
  stationSn: resolvedStationSn(raw, raw.device_sn),
26457
26480
  p2pDid: raw.p2p_did,
@@ -26459,11 +26482,12 @@ var DeviceRegistry = class {
26459
26482
  paramUpdatedAt,
26460
26483
  lastSeenMs: lastSeenMsOf(paramUpdatedAt),
26461
26484
  raw,
26462
- deviceClass: deviceClassOf(codec, c.realtime),
26485
+ deviceClass,
26463
26486
  ...c
26464
26487
  });
26465
26488
  }
26466
26489
  }
26490
+ await this.loadCloudDps(seen);
26467
26491
  this.lastRefreshPartial = partial;
26468
26492
  if (partial) {
26469
26493
  for (const prev of this.devices)
@@ -26554,9 +26578,46 @@ var DeviceRegistry = class {
26554
26578
  }
26555
26579
  return dev;
26556
26580
  }
26581
+ /**
26582
+ * Hold an AIoT robot's cloud data points: the cloud's last-known value of each one it has reported,
26583
+ * from `getDeviceRelationList`'s per-device `dps`.
26584
+ *
26585
+ * The device list carries only some of a robot's data points, so a point the robot publishes on change
26586
+ * alone (its scene list, its schedules, its consumables) has no value until it next reports. The
26587
+ * relation list holds every one, for an account the robot is shared with as well as its owner.
26588
+ *
26589
+ * Kept apart from the device list's params rather than merged into them: those are what a poll diffs,
26590
+ * and a diff retires the realtime report for that id, so a cloud value lagging the robot's last report
26591
+ * would replace it. {@link record} joins this beneath both halves. A failure keeps what was held.
26592
+ *
26593
+ * Limited to {@link CLOUD_DPS_MODELS}, the models whose relation-list `dps` are checked to carry the
26594
+ * DP numbers their realtime reports use.
26595
+ */
26596
+ async loadCloudDps(seen) {
26597
+ const robots = [...seen.values()].filter((d) => CLOUD_DPS_MODELS.has(d.model ?? ""));
26598
+ if (!robots.length)
26599
+ return;
26600
+ let res;
26601
+ try {
26602
+ res = await this.mega.getDeviceRelationList();
26603
+ } catch (e) {
26604
+ if (e instanceof SessionExpiredError)
26605
+ throw e;
26606
+ this.logger.debug(`[registry] device relation list unavailable: ${e?.message ?? e}`);
26607
+ return;
26608
+ }
26609
+ const wanted = new Set(robots.map((d) => d.sn));
26610
+ for (const entry of res.devices ?? []) {
26611
+ const sn = entry?.device?.device_sn;
26612
+ const dps = parseAiotDpReport({ payload: entry?.device?.dps });
26613
+ if (sn && wanted.has(sn) && dps)
26614
+ this.cloudDps.set(sn, dps);
26615
+ }
26616
+ }
26557
26617
  /**
26558
26618
  * Resolve a serial to a {@link DeviceRecord} with current params: starts from the device-list params, then
26559
- * overlays a fresh `get_device_param_list` when that call is available to this account.
26619
+ * overlays a fresh `get_device_param_list` when that call is available to this account. An AIoT robot's
26620
+ * cloud data points ({@link loadCloudDps}) sit beneath both, filling only ids neither carries.
26560
26621
  *
26561
26622
  * The overlay is **owner-gated** — a shared or member account is refused it for every device, permanently,
26562
26623
  * which {@link OWNER_ONLY_CODE} identifies — so when it is unavailable the device list is the source
@@ -26585,7 +26646,7 @@ var DeviceRegistry = class {
26585
26646
  let dev = this.devices.find((d) => d.sn === sn);
26586
26647
  if (!dev)
26587
26648
  throw new Error(`device ${sn} not found (have: ${this.devices.map((d) => d.sn).join(", ")})`);
26588
- const params = { ...dev.params ?? {} };
26649
+ const params = { ...this.cloudDps.get(sn), ...dev.params ?? {} };
26589
26650
  const paramUpdatedAt = { ...dev.paramUpdatedAt ?? {} };
26590
26651
  if (!this.overlayRefused.has(sn)) {
26591
26652
  try {