@mega-yfue/eufy-sdk 0.5.0-beta.8 → 0.5.0-beta.80

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.
Files changed (61) hide show
  1. package/dist/client/device-registry.d.ts +24 -2
  2. package/dist/client/eufy-mega.d.ts +75 -6
  3. package/dist/client/types.d.ts +1 -1
  4. package/dist/core/contracts.d.ts +80 -33
  5. package/dist/core/store.d.ts +4 -0
  6. package/dist/core/types.d.ts +2 -0
  7. package/dist/index.js +3754 -617
  8. package/dist/index.js.map +4 -4
  9. package/dist/model/capabilities/arming.d.ts +39 -4
  10. package/dist/model/capabilities/camera.d.ts +47 -18
  11. package/dist/model/capabilities/dp-catalog.d.ts +4 -3
  12. package/dist/model/capabilities/index.d.ts +16 -3
  13. package/dist/model/capabilities/light.d.ts +3 -0
  14. package/dist/model/capabilities/lock.d.ts +21 -7
  15. package/dist/model/capabilities/motion.d.ts +17 -2
  16. package/dist/model/capabilities/mower.d.ts +230 -0
  17. package/dist/model/capabilities/rtsp.d.ts +16 -3
  18. package/dist/model/capabilities/smart-drop.d.ts +20 -0
  19. package/dist/model/capabilities/suction.d.ts +5 -5
  20. package/dist/model/capabilities/types.d.ts +10 -0
  21. package/dist/model/capabilities/vacuum-clean.d.ts +19 -13
  22. package/dist/model/classify.d.ts +1 -2
  23. package/dist/model/device-family.d.ts +12 -0
  24. package/dist/model/param-dictionary.d.ts +6 -0
  25. package/dist/model/param-namespace.d.ts +1 -1
  26. package/dist/model/registry.d.ts +3 -2
  27. package/dist/model/types.d.ts +26 -12
  28. package/dist/transport/abortable.d.ts +8 -0
  29. package/dist/transport/http/mega-client.d.ts +34 -2
  30. package/dist/transport/mqtt/broker-discovery.d.ts +2 -1
  31. package/dist/transport/mqtt/command-router.d.ts +3 -2
  32. package/dist/transport/p2p/audio-frame.d.ts +14 -0
  33. package/dist/transport/p2p/codec.d.ts +16 -0
  34. package/dist/transport/p2p/command-router.d.ts +19 -30
  35. package/dist/transport/p2p/fmp4.d.ts +12 -2
  36. package/dist/transport/p2p/live-trace.d.ts +25 -2
  37. package/dist/transport/p2p/p2p-session.d.ts +55 -12
  38. package/dist/transport/p2p/readable-egress.d.ts +4 -0
  39. package/dist/transport/p2p/recording-download.d.ts +4 -3
  40. package/dist/transport/p2p/shared-live-source.d.ts +29 -14
  41. package/dist/transport/p2p/talkback.d.ts +21 -10
  42. package/dist/transport/p2p/video.d.ts +2 -0
  43. package/dist/transport/rtc/call-live.d.ts +41 -0
  44. package/dist/transport/rtc/command-router.d.ts +145 -0
  45. package/dist/transport/rtc/live.d.ts +76 -0
  46. package/dist/transport/rtc/peer.d.ts +122 -0
  47. package/dist/transport/rtc/portal-keepalive.d.ts +11 -0
  48. package/dist/transport/rtc/portal-packet.d.ts +72 -0
  49. package/dist/transport/rtc/ptcs-framer.d.ts +86 -0
  50. package/dist/transport/rtc/scall-sdp.d.ts +50 -0
  51. package/dist/transport/rtc/session.d.ts +99 -0
  52. package/dist/transport/rtc/signaling.d.ts +139 -0
  53. package/dist/transport/token-echo.d.ts +8 -0
  54. package/dist/transport/tuya/account.d.ts +8 -5
  55. package/dist/transport/tuya/client.d.ts +19 -5
  56. package/dist/transport/tuya/command-router.d.ts +10 -14
  57. package/dist/transport/tuya/dp-codec.d.ts +2 -23
  58. package/dist/transport/tuya/et3.d.ts +22 -0
  59. package/dist/transport/tuya/request.d.ts +18 -8
  60. package/dist/transport/tuya/sign.d.ts +7 -0
  61. package/package.json +2 -1
@@ -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
@@ -98,6 +98,8 @@ export declare class EufyMega extends EventEmitter {
98
98
  private readonly prewarmTiers;
99
99
  /** Transport-side owner of the P2P sessions + all wire senders. */
100
100
  private readonly p2p;
101
+ /** Transport-side owner of the T9000 station sessions (sibling of {@link p2p}). */
102
+ private readonly rtc;
101
103
  /** Transport-side owner of the secure-MQTT ff09 lock/garage command path (sibling of {@link p2p}). */
102
104
  private readonly mqtt;
103
105
  /** Transport-side owner of the legacy Tuya REST command path for non-AIoT vacuums (G-series). */
@@ -119,6 +121,8 @@ export declare class EufyMega extends EventEmitter {
119
121
  private readonly mapStores;
120
122
  /** Re-armed after each cloud-param poll; cancelled by {@link disconnect}. */
121
123
  private readonly pollTimer;
124
+ /** Armed while a failed push bring-up waits to be retried; cancelled by {@link disconnect}. */
125
+ private readonly pushRetryTimer;
122
126
  constructor(opts: EufyMegaOptions);
123
127
  /**
124
128
  * Builds the inbound listener that converts ThingClips DP maps to realtime capability state.
@@ -181,7 +185,8 @@ export declare class EufyMega extends EventEmitter {
181
185
  *
182
186
  * Skipped entirely for a device with a pollable cloud record, for an already-reporting device, and
183
187
  * when realtime is off — so the common path adds nothing. Resolving on timeout leaves a silent device
184
- * read-less rather than failing the lookup.
188
+ * read-less rather than failing the lookup. A Tuya clean-line device waits on the Tuya cloud read the
189
+ * bring-up started, so no broker is brought up for it.
185
190
  */
186
191
  private awaitFirstRealtimeState;
187
192
  /**
@@ -234,10 +239,24 @@ export declare class EufyMega extends EventEmitter {
234
239
  *
235
240
  * The evidence set is widened, never replaced: the ids come back through the cloud record, and a
236
241
  * record that omits a realtime-only id would otherwise un-know it and re-trigger on the next report.
242
+ *
243
+ * The device is re-resolved against the fresh record before binding. A property gated on a
244
+ * realtime-only param enters the schema only once that param is evidence, and the value that made it
245
+ * evidence was stored before the property existed, so the record's realtime params are applied again
246
+ * to land it under the property's name.
237
247
  */
238
248
  private rebindReads;
239
- /** The distinct credential scopes the current MQTT roster needs — no devices on a scope, no connection. */
249
+ /**
250
+ * The distinct credential scopes the current MQTT roster needs — no devices on a scope, no connection.
251
+ * A Tuya clean-line device (`eufy_home_tuya`) needs none: the broker refuses its topics, and its state
252
+ * is read from the Tuya cloud.
253
+ */
240
254
  private mqttScopesInUse;
255
+ /**
256
+ * Read every Tuya clean-line device's DPs from the Tuya cloud and deliver them as realtime state,
257
+ * concurrently. A failed read is reported, not fatal, and leaves that device's state as it was.
258
+ */
259
+ private readTuyaDevices;
241
260
  /**
242
261
  * Emit a semantic capability event whose name is only known at runtime (`decodeCapabilityEvent`
243
262
  * returns a plain-string `event` + `payload`). The typed `emit` overload requires a literal event
@@ -261,6 +280,8 @@ export declare class EufyMega extends EventEmitter {
261
280
  * only "differs from what was read before", which state already on hand can satisfy spuriously — and the
262
281
  * caller that has no expectation is the push path, where the signal itself is the news that a re-read is owed.
263
282
  *
283
+ * Only a write's observation throws at the deadline; a push with nothing moved answers `false`.
284
+ *
264
285
  * The cloud half is asked for through {@link DeviceRegistry.refreshedList}, never by fetching the account
265
286
  * list outright. The fetch is account-wide — one house list plus one device list per house — so a param that
266
287
  * never converges would otherwise spend a whole burst of those every iteration of this loop, and concurrent
@@ -286,6 +307,8 @@ export declare class EufyMega extends EventEmitter {
286
307
  * manager's own log, since by then nothing is waiting to receive it.
287
308
  */
288
309
  private recycleStandaloneSession;
310
+ /** Reopen a wired station that {@link recycleStandaloneSession} closed, while the generation that warmed it is current. */
311
+ private reopenWiredStation;
289
312
  /**
290
313
  * Whether the DECODED property now reads what the write asked for.
291
314
  *
@@ -390,6 +413,14 @@ export declare class EufyMega extends EventEmitter {
390
413
  * other kind is P2P.
391
414
  */
392
415
  private commandSinkFor;
416
+ /**
417
+ * The station a device's commands go through: the device itself, or the HomeBase the registry links it
418
+ * to. Answers the target record, the station record and serial, the station's `device_type`, and the
419
+ * `t9000` / `t8n00` / `t8223` family flags.
420
+ */
421
+ private stationOf;
422
+ /** The vendor DeviceType of the station a device hangs off; `undefined` for a station itself. */
423
+ private stationDeviceTypeOf;
393
424
  /**
394
425
  * Combine explicit P2P media with the optional passive push-thumbnail provider.
395
426
  *
@@ -409,10 +440,25 @@ export declare class EufyMega extends EventEmitter {
409
440
  * The `eufy_life` DP writes (smart lights) are secure-MQTT-only. `aiot-dp` routes to either the
410
441
  * Anker AIoT MQTT stack or the legacy Tuya REST router depending on the device's category
411
442
  * (`eufy_home_tuya` → Tuya, everything else → MQTT). The capability layer emits a single `aiot-dp`
412
- * kind and stays transport-agnostic; only the facade sees both sides and decides here. Everything
413
- * else is P2P.
443
+ * kind and stays transport-agnostic; only the facade sees both sides and decides here. A T9000 or T8N00
444
+ * station and the devices attached to it go over RTC, with an attached device's command refused
445
+ * unless its channel resolves back to it on the station. T8N00 uses full SDP and admits direct and
446
+ * relay candidates; T9000 retains compact SDP and relay-only candidates. A T8223 doorbell's route serves
447
+ * media only, and the RTC router refuses a command on it. Everything else is P2P.
414
448
  */
415
449
  private routeCommand;
450
+ /**
451
+ * The RTC route for a T9000 or T8N00 station or a device attached to one, or a T8223 doorbell that is its
452
+ * own station, decided by the station's model classification; `undefined` for any other device. A T8N00
453
+ * route carries its native `call` signalling; a T8223 route does too, and streams its video on a media
454
+ * track. The admin id is the station's `member.admin_user_id`, else the logged-in user.
455
+ */
456
+ private rtcRouteFor;
457
+ /**
458
+ * The RTC media provider for a station on a media-track route, or a camera attached to a T9000 whose
459
+ * `device_channel` resolves back to it on the station; `undefined` otherwise, unless an NVR camera has its own dedicated media call.
460
+ */
461
+ private rtcMediaFor;
416
462
  /**
417
463
  * List + classify devices across all houses (mega API). Each device is tagged with its API backend
418
464
  * + realtime transport. Camera/HomeBase records still appear here for inventory; driving them is
@@ -420,8 +466,9 @@ export declare class EufyMega extends EventEmitter {
420
466
  *
421
467
  * Side-effect: registers `eufy_home_tuya` devices with the Tuya command router so
422
468
  * the command dispatcher can resolve a eufy SN → Tuya devId without a separate lookup.
423
- * The Tuya id is extracted from the device's raw cloud record (`tuya_uuid`, `tuya_virtual_id`,
424
- * `tuya_device_id`, or `virtualId` fields — whichever is non-empty).
469
+ * The Tuya id is the first non-empty `tuya_uuid`, `tuya_virtual_id`, `tuya_device_id` or
470
+ * `virtualId` on the raw cloud record, else the device's serial: the app keys its Tuya device by
471
+ * the eufy device id, and a record that carries none of those fields is read under its serial.
425
472
  *
426
473
  * A partial cloud outage still resolves, with the devices that answered plus the ones already known — but a
427
474
  * session the cloud has rejected REJECTS, with {@link SessionExpiredError}. An empty list would be
@@ -524,6 +571,14 @@ export declare class EufyMega extends EventEmitter {
524
571
  private ensureRealtimeGeneration;
525
572
  /** Start all selected transports concurrently and settle their owning generation once. */
526
573
  private startRealtimeGeneration;
574
+ /**
575
+ * Bring push up again after the generation's startup attempt failed, backing off between attempts
576
+ * until one connects, the generation is superseded, or it is aborted by {@link disconnect}.
577
+ *
578
+ * The MCS socket reconnects on its own only once it has connected. The settled
579
+ * {@link RealtimeReadiness} is a startup snapshot and is not revised when a retry connects.
580
+ */
581
+ private retryPush;
527
582
  /** Resolve a generation with an immutable count snapshot; later transport completions are ignored. */
528
583
  private settleRealtimeGeneration;
529
584
  /**
@@ -566,6 +621,11 @@ export declare class EufyMega extends EventEmitter {
566
621
  * an unchanged VALUE, which is no state change to report but is fresh proof the device is alive. A
567
622
  * device absent from the previous pass is skipped — first sight is discovery, not a transition;
568
623
  * {@link deviceState} answers an initial reading.
624
+ *
625
+ * Then re-reads every Tuya clean-line robot from the Tuya cloud ({@link readTuyaDevices}): its state
626
+ * is not in the cloud record this pass diffs and no live channel carries it, so the poll is where it
627
+ * stays fresh. That read runs after the `try`, not inside it, so a cloud error on the list diff does
628
+ * not skip it; it reports its own failures and never rejects.
569
629
  */
570
630
  private pollOnce;
571
631
  /**
@@ -669,6 +729,11 @@ export declare class EufyMega extends EventEmitter {
669
729
  * — an already-open session resolves immediately.
670
730
  */
671
731
  connectStation(deviceSn: string, signal?: AbortSignal): Promise<void>;
732
+ /**
733
+ * Keep a T9000 station's RTC command session open and warm, so a command fires without a cold reconnect.
734
+ * Answers the hold's disposer; throws for any other station, and while logged out.
735
+ */
736
+ keepStationWarm(stationSn: string): () => void;
672
737
  /** Eagerly open P2P sessions for WIRED stations only (persistent — they don't drain). Battery
673
738
  * stations stay closed until an on-demand open. Best-effort per station. */
674
739
  private warmWiredP2P;
@@ -743,6 +808,9 @@ export declare class EufyMega extends EventEmitter {
743
808
  /**
744
809
  * Subscribe the devices on one credential scope; a failing subscribe is reported, not fatal (one
745
810
  * unreachable device must not stop the rest of the roster from coming up).
811
+ *
812
+ * A Tuya clean-line device (`eufy_home_tuya`) is not subscribed: its state lives on the Tuya cloud,
813
+ * and the broker refuses its `eufy_home` topics on every credential.
746
814
  */
747
815
  private subscribeMqttDevices;
748
816
  /**
@@ -860,6 +928,7 @@ export declare class EufyMega extends EventEmitter {
860
928
  * exactly the reads it has.
861
929
  */
862
930
  private fetchDpCatalog;
931
+ /** The {@link CommandContext} for a device, built from `known` when that record is already in hand. */
863
932
  private commandContext;
864
933
  /**
865
934
  * **Generic P2P request/reply query** — sends a `SET_PAYLOAD` sub-command and resolves with the
@@ -390,7 +390,7 @@ export type EufyMegaEventMap = {
390
390
  sn: string;
391
391
  property: string;
392
392
  param: number;
393
- expected?: boolean | number | string;
393
+ expected: boolean | number | string;
394
394
  observed?: boolean | number | string;
395
395
  timeoutMs: number;
396
396
  }
@@ -171,9 +171,10 @@ export declare class RecordingDownloadError extends Error {
171
171
  * A recording downloaded from the station, as elementary streams.
172
172
  *
173
173
  * `video` is Annex-B H.264 in decode order, `audio` AAC-LC 16 kHz mono in ADTS framing (absent when the
174
- * recording carried none that could be decoded). `frames` counts the distinct video frames received,
175
- * `missingFrames` the ones the camera numbered but that never arrived, and `durationMs` spans the first
176
- * to the last received frame by the camera's own timestamps; `fps` is derived from those two stamps.
174
+ * recording carried none that could be decoded). `frames` counts the distinct video sequences emitted.
175
+ * `missingFrames` counts gaps between received sequences and received sequences that yielded no usable
176
+ * picture, including at either end. `durationMs` spans the first to the last emitted picture by the
177
+ * camera's own timestamps; `fps` is derived from those stamps.
177
178
  */
178
179
  export interface RecordingDownload {
179
180
  video: Buffer;
@@ -230,7 +231,7 @@ export interface StateConvergenceFailure {
230
231
  property: string;
231
232
  param: number;
232
233
  /** The RAW param value the write asked the device to report. */
233
- expected?: boolean | number | string;
234
+ expected: boolean | number | string;
234
235
  /** What the param actually read when the deadline passed, absent where the device reported none at all. */
235
236
  observed?: boolean | number | string;
236
237
  timeoutMs: number;
@@ -248,7 +249,7 @@ export declare class StateConvergenceError extends Error {
248
249
  readonly sn: string;
249
250
  readonly property: string;
250
251
  readonly param: number;
251
- readonly expected?: boolean | number | string;
252
+ readonly expected: boolean | number | string;
252
253
  readonly observed?: boolean | number | string;
253
254
  readonly timeoutMs: number;
254
255
  constructor(failure: StateConvergenceFailure);
@@ -328,6 +329,11 @@ export type Command = {
328
329
  channel: number;
329
330
  mValue3?: number;
330
331
  form?: ScalarForm;
332
+ /**
333
+ * `"set-payload"`: a level-2 key that seals only this envelope (an RSA cipher's AES-128-ECB key)
334
+ * may seal it. Absent, the send waits for the key every level-2 sender uses.
335
+ */
336
+ level2Sender?: "set-payload";
331
337
  } | {
332
338
  kind: "p2p-privacy-burst";
333
339
  enabled: boolean;
@@ -457,7 +463,7 @@ export interface CommandSink {
457
463
  dispatch(cmd: Command): Promise<void>;
458
464
  }
459
465
  /**
460
- * Inbound Tuya DP event contract — the transport parses a ThingClips MQTT payload
466
+ * Inbound Tuya DP event contract — the transport parses the Tuya cloud DP read
461
467
  * (string-keyed object → numeric-keyed record) and delivers it through this interface. The
462
468
  * transport owns the parse; a capability owns the semantics (which DP id means what). Mirrors
463
469
  * {@link DpInboundFrame} for the AIoT MQTT path; the capability layer never names the Tuya framing.
@@ -487,6 +493,8 @@ export type VideoCodec = "h264" | "h265" | "av1";
487
493
  * headers promise decodes to no picture at all.
488
494
  */
489
495
  export interface LiveVideoFrame {
496
+ /** Source-reported unsigned 32-bit timestamp; unit and clock origin are unspecified. */
497
+ sourceTimestamp?: number;
490
498
  /** True on an IDR — a unit a consumer may begin decoding at, never a continuation of an earlier one. */
491
499
  keyframe: boolean;
492
500
  /**
@@ -541,6 +549,12 @@ export interface LiveVideoConfig {
541
549
  width: number;
542
550
  height: number;
543
551
  }
552
+ /** Video frame paired with its original SDK source-arrival time. */
553
+ export interface ReceivedVideoFrame {
554
+ readonly frame: LiveVideoFrame;
555
+ /** Wall-clock milliseconds when the shared source received the frame; preserved on replay. */
556
+ readonly timestampMs: number;
557
+ }
544
558
  /**
545
559
  * Elementary-stream audio codec of a {@link LiveAudioFrame}. The station declares it per frame as a
546
560
  * byte in the `CMD_AUDIO_FRAME` header — unlike video, nothing is sniffed. These are the three values
@@ -548,19 +562,30 @@ export interface LiveVideoConfig {
548
562
  * 7 → `mp4a.40.39`); it fails the stream on anything else.
549
563
  */
550
564
  export type AudioCodec = "aac-lc" | "aac-eld" | "g711a";
565
+ /** The codec a camera's speaker plays: `aac-eld` on a camera whose speaker plays it, `aac-lc` otherwise. */
566
+ export type TalkbackCodec = Extract<AudioCodec, "aac-lc" | "aac-eld">;
551
567
  /**
552
568
  * One audio access unit, carrying the codec the station declared for it.
553
569
  *
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.
570
+ * Sample rate and channel count are absent for `aac-lc` and `g711a`: they are not on the wire, and the v6 app
571
+ * assumes 16 kHz mono for every audio type rather than reading them. An `aac-lc` frame is one ADTS frame and
572
+ * `g711a` is raw A-law. An `aac-eld` frame is one raw access unit with no transport framing, so it carries the
573
+ * AudioSpecificConfig that decodes it.
557
574
  */
558
- export interface LiveAudioFrame {
575
+ export type LiveAudioFrame = {
559
576
  /** Codec declared in the frame header. */
560
- codec: AudioCodec;
561
- /** Elementary-stream bytes (ADTS-framed for the two AAC profiles). */
577
+ codec: "aac-lc" | "g711a";
578
+ /** One ADTS frame for `aac-lc`, raw A-law samples for `g711a`. */
562
579
  data: Buffer;
563
- }
580
+ config?: undefined;
581
+ } | {
582
+ /** Codec declared in the frame header. */
583
+ codec: "aac-eld";
584
+ /** One raw AAC-ELD access unit. */
585
+ data: Buffer;
586
+ /** AudioSpecificConfig (ISO/IEC 14496-3) that decodes `data`. */
587
+ config: Buffer;
588
+ };
564
589
  /**
565
590
  * One fragmented-MP4 (CMAF) output unit from the native muxer. `init` (the `ftyp`+`moov` init
566
591
  * segment) is present exactly once, on the first fragment; every fragment carries a `moof`+`mdat`
@@ -709,6 +734,10 @@ export interface LiveStreamConsumer extends LiveStreamHandle {
709
734
  export interface AbortableCall {
710
735
  signal?: AbortSignal;
711
736
  }
737
+ /** Select an optical view; absence selects view 0. */
738
+ export interface MediaSourceSelection {
739
+ view?: number;
740
+ }
712
741
  export interface SharedSourceHints {
713
742
  /**
714
743
  * Power source, a runtime device fact (`"battery"` incl. solar, or `"wired"`) — never a device-family
@@ -734,6 +763,8 @@ export interface SharedSourceHints {
734
763
  * because an unbound model has no live client (hence `?.` at the call site).
735
764
  */
736
765
  export interface MediaProvider {
766
+ /** Optical view identifiers supported by this bound media route. */
767
+ views?: readonly number[];
737
768
  /**
738
769
  * Return the latest validated push thumbnail retained in memory. This passive operation performs no
739
770
  * network, storage, P2P, live-media, or transcoding work at call time. It rejects with
@@ -760,7 +791,7 @@ export interface MediaProvider {
760
791
  timeoutMs?: number;
761
792
  collectMs?: number;
762
793
  skipKeyframes?: number;
763
- } & SharedSourceHints & AbortableCall): Promise<{
794
+ } & SharedSourceHints & AbortableCall & MediaSourceSelection): Promise<{
764
795
  jpeg: Buffer;
765
796
  width: number;
766
797
  height: number;
@@ -792,7 +823,7 @@ export interface MediaProvider {
792
823
  * stream?.stop(); // detach this consumer
793
824
  * ```
794
825
  */
795
- live(opts?: SharedSourceHints & AbortableCall & Record<string, unknown>): Promise<LiveStreamConsumer>;
826
+ live(opts?: SharedSourceHints & AbortableCall & MediaSourceSelection & Record<string, unknown>): Promise<LiveStreamConsumer>;
796
827
  /**
797
828
  * Record `seconds` of video → an mp4/h264 buffer.
798
829
  *
@@ -809,13 +840,16 @@ export interface MediaProvider {
809
840
  }): Promise<Buffer>;
810
841
  /**
811
842
  * Open a video-only `node:stream` Readable over a shared source consumer — raw Annex-B bytes
812
- * (default) or `objectMode` {@link LiveVideoFrame}s. Audio is available separately through
813
- * {@link live} or muxed through {@link recordFragments}; it is never interleaved into raw video.
814
- * The caller owns the Readable's lifetime, and destroying it releases the shared pull.
843
+ * (default) or `objectMode` {@link LiveVideoFrame}s. With objectMode and arrivalTime, it yields
844
+ * {@link ReceivedVideoFrame} envelopes whose original source-arrival time survives retained replay.
845
+ * Raw mode ignores arrivalTime. Audio is available separately through {@link live} or muxed through
846
+ * {@link recordFragments}; it is never interleaved into raw video. The caller owns the Readable's
847
+ * lifetime, and destroying it releases the shared pull.
815
848
  */
816
849
  openReadable?(opts?: {
817
850
  objectMode?: boolean;
818
- } & SharedSourceHints & AbortableCall): Promise<import("node:stream").Readable>;
851
+ arrivalTime?: boolean;
852
+ } & SharedSourceHints & AbortableCall & MediaSourceSelection): Promise<import("node:stream").Readable>;
819
853
  /**
820
854
  * Continuously record the live feed as fragmented-MP4 (CMAF). The caller-owned
821
855
  * {@link FragmentRecordingHandle} yields an init segment then keyframe-bounded media fragments,
@@ -855,6 +889,7 @@ export interface MediaProvider {
855
889
  */
856
890
  p2pQuery?(subCmd: number, opts?: {
857
891
  timeoutMs?: number;
892
+ payload?: Record<string, unknown>;
858
893
  }): Promise<Record<string, unknown>>;
859
894
  /**
860
895
  * Generic control-payload request/reply query: send a `CONTROL_PAYLOAD` (1700) `{commandType,data}`
@@ -868,10 +903,11 @@ export interface MediaProvider {
868
903
  }): Promise<Record<string, unknown>>;
869
904
  }
870
905
  /**
871
- * An encoder that turns raw PCM into AAC-LC frames, supplied by the CALLER. The SDK ships none: the
872
- * device's audio path is fixed at AAC-LC 16 kHz mono, and every plausible encoder is either a native
906
+ * An encoder that turns raw PCM into AAC-LC frames, supplied by the CALLER. The SDK ships none: an
907
+ * `aac-lc` speaker plays AAC-LC 16 kHz mono, and every plausible encoder is either a native
873
908
  * dependency or an external process, both of which belong to the host rather than to a protocol SDK.
874
- * It is unnecessary where the audio is already AAC — see {@link TalkbackHandle}.
909
+ * It is unnecessary where the audio is already AAC — see {@link TalkbackHandle}. A camera whose speaker
910
+ * plays `aac-eld` refuses an encoder: a talkback opened with one is rejected before the speaker path opens.
875
911
  *
876
912
  * `encode` receives 16-bit little-endian mono PCM at 16 kHz and returns whole ADTS frames, zero or
877
913
  * more per call (an encoder buffers until it has a full 1024-sample block). `flush` drains a partial
@@ -886,17 +922,19 @@ export interface AacEncoder {
886
922
  * A live talkback session: audio pushed from the host to a camera's speaker, the mirror of
887
923
  * {@link LiveStreamHandle}'s inbound feed.
888
924
  *
889
- * Audio must be **AAC-LC, 16 kHz, mono, in ADTS frames** — what the device's path is fixed at, so a
890
- * stream at another rate or channel count is rejected rather than resampled (it would otherwise play
891
- * at the wrong pitch and speed). Feed it either way:
925
+ * Audio must be in the codec the camera's speaker plays, {@link TalkbackHandle.codec}:
892
926
  *
893
- * - **ADTS AAC** — the default. Chunk boundaries are irrelevant; frames are recovered from the
894
- * stream, so piping an encoder's output straight in works.
895
- * - **PCM** — only when the handle was opened with an {@link AacEncoder}, which then does the
896
- * conversion. `write` takes 16-bit little-endian mono PCM at 16 kHz instead.
927
+ * - **`aac-lc`:** AAC-LC, 16 kHz, mono, in ADTS frames, through `write` / `writable()`. Chunk boundaries are
928
+ * irrelevant; frames are recovered from the stream. With an {@link AacEncoder}, `write` takes 16-bit
929
+ * little-endian mono PCM at 16 kHz instead.
930
+ * - **`aac-eld`:** one raw AAC-ELD access unit with LD-SBR (16 kHz, mono, 512 samples) per `write`, or per
931
+ * object-mode `writable()` write. No encoder applies.
897
932
  *
898
- * Frames are **paced** at their own playback rate (64 ms each) rather than flushed as fast as they
899
- * arrive, so feeding a file plays it at speed instead of overrunning the device. A live source keeps
933
+ * An `aac-lc` stream at another rate or channel count is rejected rather than resampled. An `aac-eld` unit longer
934
+ * than the device accepts is refused with an `error`.
935
+ *
936
+ * Frames are **paced** at their own playback rate (64 ms for `aac-lc`, 32 ms for `aac-eld`) rather than flushed as
937
+ * fast as they arrive, so feeding a file plays it at speed instead of overrunning the device. A live source keeps
900
938
  * the queue near-empty and is unaffected.
901
939
  *
902
940
  * @example
@@ -908,7 +946,16 @@ export interface AacEncoder {
908
946
  * ```
909
947
  */
910
948
  export interface TalkbackHandle {
911
- /** Queue audio — ADTS frames, or PCM when an encoder was supplied. Partial frames are held. */
949
+ /**
950
+ * The codec this camera's speaker plays: `aac-eld` on a camera whose speaker plays it and whose stream on the same
951
+ * media session carries `aac-eld`, `aac-lc` otherwise, including when that stream carries no audio. It decides
952
+ * what {@link write} and {@link writable} take.
953
+ */
954
+ readonly codec: TalkbackCodec;
955
+ /**
956
+ * Queue audio: ADTS frames or PCM for `aac-lc`, one access unit per call for `aac-eld`. Partial ADTS frames are
957
+ * held.
958
+ */
912
959
  write(chunk: Buffer): void;
913
960
  /**
914
961
  * A `node:stream` Writable over {@link write}, for piping a file or an encoder's stdout. Applies
@@ -931,7 +978,7 @@ export interface TalkbackHandle {
931
978
  *
932
979
  * This deliberately does NOT fire on a merely-empty queue. A realtime source keeps the queue near
933
980
  * empty by design, so "queue is empty" arrives after the very first frame and stopping on it would
934
- * cut the clip to 64 ms. {@link pending} is the instantaneous depth.
981
+ * cut the clip to one frame. {@link pending} is the instantaneous depth.
935
982
  */
936
983
  on(event: "finished", listener: () => void): this;
937
984
  /**
@@ -16,6 +16,10 @@ export interface PersistedSession {
16
16
  authToken: string;
17
17
  geoKey?: string;
18
18
  region: RegionShard;
19
+ /** The `countryCode` the session was logged in with; a different configured one logs in again. */
20
+ countryCode?: string;
21
+ /** The host `estimate_domain` answered; it names shards the region classification cannot (`ie`). */
22
+ estimatedDomain?: string;
19
23
  openudid: string;
20
24
  /** This install's reported device model + media user-agent, generated once and reused. */
21
25
  phoneModel?: string;
@@ -55,6 +55,8 @@ export interface EufyDevice {
55
55
  * every attached sensor of a T8010 — and serves only as a fallback for a device naming no parent.
56
56
  */
57
57
  stationSn?: string;
58
+ /** The device's channel on its station, as its record states it (`device_channel`); absent where it states none. */
59
+ channel?: number;
58
60
  /** Present (and non-empty) for P2P devices. */
59
61
  p2pDid?: string;
60
62
  /**