@mega-yfue/eufy-sdk 0.5.0-beta.13 → 0.5.0-beta.15

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.
@@ -92,6 +92,8 @@ export declare class DeviceRegistry {
92
92
  private overlayRefusalReported;
93
93
  /** Per-serial capability cache for {@link capabilitiesForDevice}; `null` = negative hit. */
94
94
  private readonly deviceCapsCache;
95
+ /** Per-serial cloud data points of an AIoT robot, joined beneath the params in {@link record} — see {@link loadCloudDps}. */
96
+ private readonly cloudDps;
95
97
  /** Per-serial realtime state, keyed by param id in the device's own namespace (see {@link DeviceRecord.dpParams}). */
96
98
  private readonly dpParams;
97
99
  /** Callers blocked in {@link awaitRealtimeState}, released by the device's first report. */
@@ -181,9 +183,26 @@ export declare class DeviceRegistry {
181
183
  * transport stacks own the claim predicate over the record.
182
184
  */
183
185
  require(sn: string): EufyDevice;
186
+ /**
187
+ * Hold an AIoT robot's cloud data points: the cloud's last-known value of each one it has reported,
188
+ * from `getDeviceRelationList`'s per-device `dps`.
189
+ *
190
+ * The device list carries only some of a robot's data points, so a point the robot publishes on change
191
+ * alone (its scene list, its schedules, its consumables) has no value until it next reports. The
192
+ * relation list holds every one, for an account the robot is shared with as well as its owner.
193
+ *
194
+ * Kept apart from the device list's params rather than merged into them: those are what a poll diffs,
195
+ * and a diff retires the realtime report for that id, so a cloud value lagging the robot's last report
196
+ * would replace it. {@link record} joins this beneath both halves. A failure keeps what was held.
197
+ *
198
+ * Limited to {@link CLOUD_DPS_MODELS}, the models whose relation-list `dps` are checked to carry the
199
+ * DP numbers their realtime reports use.
200
+ */
201
+ private loadCloudDps;
184
202
  /**
185
203
  * 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.
204
+ * overlays a fresh `get_device_param_list` when that call is available to this account. An AIoT robot's
205
+ * cloud data points ({@link loadCloudDps}) sit beneath both, filling only ids neither carries.
187
206
  *
188
207
  * The overlay is **owner-gated** — a shared or member account is refused it for every device, permanently,
189
208
  * which {@link OWNER_ONLY_CODE} identifies — so when it is unavailable the device list is the source
@@ -551,16 +551,25 @@ export type AudioCodec = "aac-lc" | "aac-eld" | "g711a";
551
551
  /**
552
552
  * One audio access unit, carrying the codec the station declared for it.
553
553
  *
554
- * Sample rate and channel count are deliberately absent: they are not on the wire. The v6 app assumes
555
- * 16 kHz mono for every audio type rather than reading them, so the SDK does not invent fields the
556
- * device never sent.
554
+ * Sample rate and channel count are absent for `aac-lc` and `g711a`: they are not on the wire, and the v6 app
555
+ * assumes 16 kHz mono for every audio type rather than reading them. An `aac-lc` frame is one ADTS frame and
556
+ * `g711a` is raw A-law. An `aac-eld` frame is one raw access unit with no transport framing, so it carries the
557
+ * AudioSpecificConfig that decodes it.
557
558
  */
558
- export interface LiveAudioFrame {
559
+ export type LiveAudioFrame = {
559
560
  /** Codec declared in the frame header. */
560
- codec: AudioCodec;
561
- /** Elementary-stream bytes (ADTS-framed for the two AAC profiles). */
561
+ codec: "aac-lc" | "g711a";
562
+ /** One ADTS frame for `aac-lc`, raw A-law samples for `g711a`. */
562
563
  data: Buffer;
563
- }
564
+ config?: undefined;
565
+ } | {
566
+ /** Codec declared in the frame header. */
567
+ codec: "aac-eld";
568
+ /** One raw AAC-ELD access unit. */
569
+ data: Buffer;
570
+ /** AudioSpecificConfig (ISO/IEC 14496-3) that decodes `data`. */
571
+ config: Buffer;
572
+ };
564
573
  /**
565
574
  * One fragmented-MP4 (CMAF) output unit from the native muxer. `init` (the `ftyp`+`moov` init
566
575
  * segment) is present exactly once, on the first fragment; every fragment carries a `moof`+`mdat`
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
  *
@@ -17931,6 +17942,7 @@ var AUDIO_HEADER_LEN = 16;
17931
17942
  var AUDIO_TYPE_OFFSET = 5;
17932
17943
  var SC4 = Buffer.from([0, 0, 0, 1]);
17933
17944
  var SC3 = Buffer.from([0, 0, 1]);
17945
+ var AAC_ELD_AUDIO_SPECIFIC_CONFIG = Buffer.from("f8f03000", "hex");
17934
17946
  var AUDIO_CODECS = { 0: "aac-lc", 2: "g711a", 7: "aac-eld" };
17935
17947
  var MAX_TRACED_DECODE_FAILURES = 3;
17936
17948
  var DEFAULT_KEEPALIVE_MS = 3e3;
@@ -18159,7 +18171,7 @@ var LiveStream = class extends EventEmitter3 {
18159
18171
  if (!codec) {
18160
18172
  this.logger.debug(`[live] dropping audio frame: unknown codec id ${codecId ?? "missing"}`);
18161
18173
  } else {
18162
- this.emit("audio", { codec, data: audio });
18174
+ this.emit("audio", codec === "aac-eld" ? { codec, data: audio, config: AAC_ELD_AUDIO_SPECIFIC_CONFIG } : { codec, data: audio });
18163
18175
  }
18164
18176
  }
18165
18177
  }
@@ -20177,7 +20189,7 @@ var CODEC_BOXES = {
20177
20189
  h264: { sample: "avc1", config: "avcC" },
20178
20190
  h265: { sample: "hvc1", config: "hvcC" }
20179
20191
  };
20180
- var AAC_ELD_SAMPLES_PER_FRAME = 512;
20192
+ var AAC_ELD_SAMPLES_PER_FRAME = 480;
20181
20193
  var ADTS_FREQUENCY_INDEX_16K = 8;
20182
20194
  var ADTS_CHANNELS_MONO = 1;
20183
20195
  var Fmp4Muxer = class {
@@ -20195,6 +20207,8 @@ var Fmp4Muxer = class {
20195
20207
  lastVideoTimestampMs;
20196
20208
  audioRequested;
20197
20209
  audioCodec;
20210
+ /** Decoder config of an AAC-ELD track, taken from its frames; absent for AAC-LC, whose `esds` config is fixed. */
20211
+ audioConfig;
20198
20212
  audioDisabled = false;
20199
20213
  audioSamples = [];
20200
20214
  audioBaseDecodeTime = 0;
@@ -20261,7 +20275,10 @@ var Fmp4Muxer = class {
20261
20275
  }
20262
20276
  return void 0;
20263
20277
  }
20264
- /** Add one station-declared AAC access unit to the audio track. G.711 remains available via `live()`. */
20278
+ /**
20279
+ * Add one station-declared AAC access unit to the audio track: an ADTS frame for AAC-LC, a raw access unit
20280
+ * described by its frame's config for AAC-ELD. G.711 remains available via `live()`.
20281
+ */
20265
20282
  pushAudio(frame, timestampMs = now()) {
20266
20283
  if (!this.audioRequested || this.audioDisabled)
20267
20284
  return void 0;
@@ -20277,12 +20294,19 @@ var Fmp4Muxer = class {
20277
20294
  if (this.audioCodec && this.audioCodec !== frame.codec) {
20278
20295
  return this.disableAudio();
20279
20296
  }
20280
- const header = parseAdtsHeader(frame.data);
20281
- if (!header || header.frameLength > frame.data.length) {
20282
- throw new Error(`fMP4 muxer: ${frame.codec} frame is not a complete ADTS access unit`);
20283
- }
20284
- if (header.frequencyIndex !== ADTS_FREQUENCY_INDEX_16K || header.channels !== ADTS_CHANNELS_MONO) {
20285
- return this.disableAudio();
20297
+ let sample;
20298
+ if (frame.codec === "aac-eld") {
20299
+ this.audioConfig = frame.config;
20300
+ sample = frame.data;
20301
+ } else {
20302
+ const header = parseAdtsHeader(frame.data);
20303
+ if (!header || header.frameLength > frame.data.length) {
20304
+ throw new Error(`fMP4 muxer: ${frame.codec} frame is not a complete ADTS access unit`);
20305
+ }
20306
+ if (header.frequencyIndex !== ADTS_FREQUENCY_INDEX_16K || header.channels !== ADTS_CHANNELS_MONO) {
20307
+ return this.disableAudio();
20308
+ }
20309
+ sample = frame.data.subarray(header.headerLength, header.frameLength);
20286
20310
  }
20287
20311
  this.audioCodec = frame.codec;
20288
20312
  this.firstAudioTimestampMs ??= timestampMs;
@@ -20294,7 +20318,7 @@ var Fmp4Muxer = class {
20294
20318
  this.audioSamples[this.audioSamples.length - 1].duration = measured;
20295
20319
  }
20296
20320
  this.audioSamples.push({
20297
- data: frame.data.subarray(header.headerLength, header.frameLength),
20321
+ data: sample,
20298
20322
  duration: frame.codec === "aac-eld" ? AAC_ELD_SAMPLES_PER_FRAME : AAC_SAMPLES_PER_FRAME,
20299
20323
  keyframe: true
20300
20324
  });
@@ -20567,7 +20591,7 @@ var Fmp4Muxer = class {
20567
20591
  return box("mp4a", head, this.esds());
20568
20592
  }
20569
20593
  esds() {
20570
- const config = this.audioCodec === "aac-eld" ? Buffer.from([248, 240, 32]) : Buffer.from([20, 8]);
20594
+ const config = this.audioConfig ?? Buffer.from([20, 8]);
20571
20595
  const specific = descriptor(5, config);
20572
20596
  const decoder = descriptor(4, Buffer.concat([Buffer.from([64, 21, 0, 0, 0]), u32(64e3), u32(32e3), specific]));
20573
20597
  const sl = descriptor(6, Buffer.from([2]));
@@ -26318,6 +26342,7 @@ function deviceClassOf(codec, realtime) {
26318
26342
  return cls === "camera" && realtime !== "p2p" ? "other" : cls;
26319
26343
  }
26320
26344
  var LIST_REUSE_MS = 5e3;
26345
+ var CLOUD_DPS_MODELS = /* @__PURE__ */ new Set(["T2351"]);
26321
26346
  var DeviceRegistry = class {
26322
26347
  mega;
26323
26348
  onError;
@@ -26342,6 +26367,8 @@ var DeviceRegistry = class {
26342
26367
  overlayRefusalReported = false;
26343
26368
  /** Per-serial capability cache for {@link capabilitiesForDevice}; `null` = negative hit. */
26344
26369
  deviceCapsCache = /* @__PURE__ */ new Map();
26370
+ /** Per-serial cloud data points of an AIoT robot, joined beneath the params in {@link record} — see {@link loadCloudDps}. */
26371
+ cloudDps = /* @__PURE__ */ new Map();
26345
26372
  /** Per-serial realtime state, keyed by param id in the device's own namespace (see {@link DeviceRecord.dpParams}). */
26346
26373
  dpParams = /* @__PURE__ */ new Map();
26347
26374
  /** Callers blocked in {@link awaitRealtimeState}, released by the device's first report. */
@@ -26451,6 +26478,7 @@ var DeviceRegistry = class {
26451
26478
  });
26452
26479
  }
26453
26480
  }
26481
+ await this.loadCloudDps(seen);
26454
26482
  this.lastRefreshPartial = partial;
26455
26483
  if (partial) {
26456
26484
  for (const prev of this.devices)
@@ -26541,9 +26569,46 @@ var DeviceRegistry = class {
26541
26569
  }
26542
26570
  return dev;
26543
26571
  }
26572
+ /**
26573
+ * Hold an AIoT robot's cloud data points: the cloud's last-known value of each one it has reported,
26574
+ * from `getDeviceRelationList`'s per-device `dps`.
26575
+ *
26576
+ * The device list carries only some of a robot's data points, so a point the robot publishes on change
26577
+ * alone (its scene list, its schedules, its consumables) has no value until it next reports. The
26578
+ * relation list holds every one, for an account the robot is shared with as well as its owner.
26579
+ *
26580
+ * Kept apart from the device list's params rather than merged into them: those are what a poll diffs,
26581
+ * and a diff retires the realtime report for that id, so a cloud value lagging the robot's last report
26582
+ * would replace it. {@link record} joins this beneath both halves. A failure keeps what was held.
26583
+ *
26584
+ * Limited to {@link CLOUD_DPS_MODELS}, the models whose relation-list `dps` are checked to carry the
26585
+ * DP numbers their realtime reports use.
26586
+ */
26587
+ async loadCloudDps(seen) {
26588
+ const robots = [...seen.values()].filter((d) => CLOUD_DPS_MODELS.has(d.model ?? ""));
26589
+ if (!robots.length)
26590
+ return;
26591
+ let res;
26592
+ try {
26593
+ res = await this.mega.getDeviceRelationList();
26594
+ } catch (e) {
26595
+ if (e instanceof SessionExpiredError)
26596
+ throw e;
26597
+ this.logger.debug(`[registry] device relation list unavailable: ${e?.message ?? e}`);
26598
+ return;
26599
+ }
26600
+ const wanted = new Set(robots.map((d) => d.sn));
26601
+ for (const entry of res.devices ?? []) {
26602
+ const sn = entry?.device?.device_sn;
26603
+ const dps = parseAiotDpReport({ payload: entry?.device?.dps });
26604
+ if (sn && wanted.has(sn) && dps)
26605
+ this.cloudDps.set(sn, dps);
26606
+ }
26607
+ }
26544
26608
  /**
26545
26609
  * Resolve a serial to a {@link DeviceRecord} with current params: starts from the device-list params, then
26546
- * overlays a fresh `get_device_param_list` when that call is available to this account.
26610
+ * overlays a fresh `get_device_param_list` when that call is available to this account. An AIoT robot's
26611
+ * cloud data points ({@link loadCloudDps}) sit beneath both, filling only ids neither carries.
26547
26612
  *
26548
26613
  * The overlay is **owner-gated** — a shared or member account is refused it for every device, permanently,
26549
26614
  * which {@link OWNER_ONLY_CODE} identifies — so when it is unavailable the device list is the source
@@ -26572,7 +26637,7 @@ var DeviceRegistry = class {
26572
26637
  let dev = this.devices.find((d) => d.sn === sn);
26573
26638
  if (!dev)
26574
26639
  throw new Error(`device ${sn} not found (have: ${this.devices.map((d) => d.sn).join(", ")})`);
26575
- const params = { ...dev.params ?? {} };
26640
+ const params = { ...this.cloudDps.get(sn), ...dev.params ?? {} };
26576
26641
  const paramUpdatedAt = { ...dev.paramUpdatedAt ?? {} };
26577
26642
  if (!this.overlayRefused.has(sn)) {
26578
26643
  try {