@mega-yfue/eufy-sdk 0.5.0-beta.3 → 0.5.0-beta.30

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 (39) hide show
  1. package/dist/client/device-registry.d.ts +24 -2
  2. package/dist/client/eufy-mega.d.ts +50 -6
  3. package/dist/client/types.d.ts +1 -1
  4. package/dist/core/contracts.d.ts +52 -26
  5. package/dist/core/store.d.ts +2 -0
  6. package/dist/core/types.d.ts +2 -0
  7. package/dist/index.js +2271 -259
  8. package/dist/index.js.map +4 -4
  9. package/dist/model/capabilities/arming.d.ts +31 -4
  10. package/dist/model/capabilities/camera.d.ts +24 -10
  11. package/dist/model/capabilities/dp-catalog.d.ts +4 -3
  12. package/dist/model/capabilities/light.d.ts +3 -0
  13. package/dist/model/capabilities/motion.d.ts +5 -0
  14. package/dist/model/capabilities/suction.d.ts +5 -5
  15. package/dist/model/capabilities/types.d.ts +6 -0
  16. package/dist/model/capabilities/vacuum-clean.d.ts +19 -13
  17. package/dist/model/classify.d.ts +1 -2
  18. package/dist/model/device-family.d.ts +4 -0
  19. package/dist/model/registry.d.ts +3 -2
  20. package/dist/model/types.d.ts +6 -0
  21. package/dist/transport/abortable.d.ts +8 -0
  22. package/dist/transport/http/mega-client.d.ts +26 -0
  23. package/dist/transport/p2p/command-router.d.ts +14 -26
  24. package/dist/transport/p2p/fmp4.d.ts +6 -1
  25. package/dist/transport/p2p/live-stream.d.ts +3 -0
  26. package/dist/transport/p2p/p2p-session.d.ts +22 -3
  27. package/dist/transport/p2p/recording-download.d.ts +4 -3
  28. package/dist/transport/p2p/shared-live-source.d.ts +9 -0
  29. package/dist/transport/p2p/talkback.d.ts +21 -10
  30. package/dist/transport/p2p/video.d.ts +5 -2
  31. package/dist/transport/rtc/command-router.d.ts +109 -0
  32. package/dist/transport/rtc/live.d.ts +69 -0
  33. package/dist/transport/rtc/peer.d.ts +101 -0
  34. package/dist/transport/rtc/portal-packet.d.ts +72 -0
  35. package/dist/transport/rtc/ptcs-framer.d.ts +86 -0
  36. package/dist/transport/rtc/scall-sdp.d.ts +42 -0
  37. package/dist/transport/rtc/session.d.ts +88 -0
  38. package/dist/transport/rtc/signaling.d.ts +139 -0
  39. package/package.json +2 -1
@@ -79,7 +79,8 @@ export declare const ARMING_CMD: {
79
79
  */
80
80
  readonly SET_ARMING: 1224;
81
81
  /**
82
- * The per-mode alarm/arm-delay configuration write. ✅ WIRE CAPTURED live on a T8030 (
82
+ * One guard mode's per-device action table (app `CMD_SET_ALL_ACTION`), which carries the alarm delays
83
+ * and each device's actions in that mode. ✅ WIRE CAPTURED live on a T8030 (
83
84
  * 2026-07-23, both directions): a **bare
84
85
  * JSON frame, no `1350`/`1700` envelope** — outer P2P cmd IS `1255` itself, station channel 255,
85
86
  * plaintext `{account_id, count_down_alarm:{channel_list,delay_time},
@@ -96,15 +97,23 @@ export declare const ARMING_CMD: {
96
97
  * corresponding GET command (`1310`/inner cmd `40003`, sent by the app right before editing) always
97
98
  * replied `{count:0,data:null}` in every capture — genuinely empty, not a decrypt failure (confirmed
98
99
  * via the same bidirectional decrypt this finding used) — so the app does NOT read the current
99
- * config this way; how it does is still unknown. Without a working GET, safely PATCHING just one
100
- * channel in or out of an existing list isn't possible without risking clobbering the rest — so
100
+ * config this way; a T8010 reports it on its own params instead, see below. Without a working GET,
101
+ * safely PATCHING just one channel in or out of an existing list isn't possible without risking
102
+ * clobbering the rest — so
101
103
  * this ships as a caller-supplies-everything write instead of guessing a merge.
102
104
  * `devices`/`siren_sensor_action` are even less understood (raw per-device action codes, meaning
103
105
  * unconfirmed) and MUST come from a value independently read/captured for the target mode —
104
106
  * see `AlarmDelayConfig`'s field docs.
107
+ *
108
+ * ✅ On a T8010 the app writes the same command for the Device Control screen: header channel 0, the
109
+ * whole table of the mode `{mode_id, devices, count_down_alarm, count_down_arm}`, acked by an all-zero
110
+ * 1255 reply. The T8010 reports each table on its own params, Home on 1157 and Away on 1158, and the
111
+ * reported table follows a write within 15 s. See {@link DEVICE_NOTIFY_FLAG}.
105
112
  */
106
- readonly ALARM_DELAY_CONFIG: 1255;
113
+ readonly SET_ALL_ACTION: 1255;
107
114
  };
115
+ /** The guard modes whose device notification is read and written: both replayed on a T8010. */
116
+ export type DeviceNotificationMode = Extract<AlarmDelayMode, "home" | "away">;
108
117
  /**
109
118
  * Alarm-delay durations the app's OWN picker UI offers — `AlarmDelaySeconds` is both the const
110
119
  * value-object (`AlarmDelaySeconds.sec45`) and the union type of its values, so callers pass the
@@ -225,6 +234,24 @@ export declare const ARMING_MEMBERS: {
225
234
  * would be the same unverified guess `setMode` refuses.
226
235
  */
227
236
  readonly setAlarmDelayConfig: import("./members.js").MethodMember<(mode: AlarmDelayMode, config: AlarmDelayConfig) => Promise<void>>;
237
+ /**
238
+ * Whether the device on `channel` sends a push notification in `mode`: its {@link DEVICE_NOTIFY_FLAG}
239
+ * in the action table the station reports for that mode. `undefined` where the station reports no table
240
+ * or the table does not list `channel`. A device's channel is `EufyDevice.channel`.
241
+ */
242
+ readonly deviceNotification: import("./members.js").MethodMember<(mode: DeviceNotificationMode, channel: number) => boolean | undefined> & {
243
+ available: (ctx: CommandContext) => boolean;
244
+ };
245
+ /**
246
+ * Turn the push notification of the device on `channel` on or off in `mode` (cmd
247
+ * {@link ARMING_CMD.SET_ALL_ACTION}): the station's reported table written back with that one flag
248
+ * changed. Rejects, sending nothing, where the table does not list `channel`. A write builds on the last
249
+ * one sent for that mode until the station reports a table observed 15 s after it, so a second write does
250
+ * not revert it; a write whose dispatch rejects is not built on.
251
+ */
252
+ readonly setDeviceNotification: import("./members.js").MethodMember<(mode: DeviceNotificationMode, channel: number, on: boolean) => Promise<void>> & {
253
+ available: (ctx: CommandContext) => boolean;
254
+ };
228
255
  };
229
256
  /**
230
257
  * `arming` — guard/arming mode. `armingMode` (see {@link ARMING_CMD.SET_ARMING}) has a verified read/write
@@ -8,6 +8,8 @@ import { type Command, type MediaProvider } from "../../core/contracts.js";
8
8
  * `transport/p2p/commands.ts`). Each entry notes its app `CommandType` name.
9
9
  */
10
10
  export declare const CAMERA_CMD: {
11
+ /** Camera-reported Wi-Fi signal (app `CMD_GET_WIFI_RSSI`). */
12
+ readonly WIFI_RSSI: 1142;
11
13
  /** Camera on/off. Read and write polarity depend on the family and topology. */
12
14
  readonly CAMERA_ENABLE: 1035;
13
15
  /** Wrapped T8410 power switch on HomeBase 3. */
@@ -271,6 +273,14 @@ export type CameraActions = Surface<typeof CAMERA_MEMBERS> & {
271
273
  * @internal
272
274
  */
273
275
  export declare const CAMERA_MEMBERS: {
276
+ /** Reads the latest cached report and exposes no setter. */
277
+ readonly wifiRssi: {
278
+ readonly param: 1142;
279
+ readonly type: "number";
280
+ readonly kind: "scalar";
281
+ readonly provenance: "apk";
282
+ readonly description: "Camera-reported Wi-Fi signal; physical units and a quality scale are unspecified.";
283
+ };
274
284
  /**
275
285
  * The default READ is the *disable*-bit convention (1035 "0" ⇒ ON, 2001 direct); the WRITE polarity is
276
286
  * family-dependent — see `powerValue` / `isEnableBitPolarity`. Battery/solo cams report the state under
@@ -435,25 +445,29 @@ export declare const CAMERA_MEMBERS: {
435
445
  * Live-view quality, the pair to {@link CAMERA_MEMBERS.recordingQuality} and a different setting on a
436
446
  * different wire: changing one leaves the other's parameter untouched.
437
447
  *
438
- * The READ is 1020, which reports the resolved tier as a plain integer. 1020 is also the name the
439
- * param dictionary already gives that id, and `Device` keys state by NAME — a member claiming 2730
440
- * under this name would collide with it on a device reporting both.
448
+ * The READ is 1020, which reports the resolved tier as a plain integer, while the WRITE rides 2730's
449
+ * `1350` payload. They are two ids for one setting because 2730 carries a per-mode config and 1020
450
+ * carries the tier in force; the tier is what this property means. 1020 is also the name the param
451
+ * dictionary already gives that id, and `Device` keys state by NAME — a member claiming 2730 under
452
+ * this name would collide with it on a device reporting both.
441
453
  *
442
- * `unverified: true` with no `write`: the app's 2730 frame is captured byte-exact, but replaying it
443
- * from this SDK produced no observable change on a standalone T8171, where the recording sibling's
444
- * 2731 write on the same envelope and the same session does land. The one difference the capture
445
- * shows is that the app's frame carries no `mChannel`/`mValue3`, which this envelope always emits.
446
- * Until a send is confirmed, the setter is absent — a control that silently does nothing is worse
447
- * than none — and the read ships ahead of it, which verification per direction allows.
454
+ * `transaction` is part of the frame that was captured, so it is sent. The recording sibling's own
455
+ * confirmed frame carries no such field; neither is inferred from the other.
448
456
  */
449
457
  readonly streamingQuality: {
458
+ readonly accepts: StreamingQualityName;
450
459
  readonly param: 1020;
451
460
  readonly type: "number";
452
461
  readonly kind: "enum";
453
462
  readonly enumValues: Readonly<Record<number, string>>;
454
463
  readonly provenance: "verified";
455
- readonly unverified: true;
464
+ readonly args: readonly [{
465
+ readonly name: "quality";
466
+ readonly kind: "enum";
467
+ readonly description: "A tier, or the name it maps to. 0 is Auto.";
468
+ }];
456
469
  readonly description: string;
470
+ readonly write: (v: string | number | boolean, ctx: CommandContext) => Command | undefined;
457
471
  };
458
472
  /**
459
473
  * `type` is how the value is STORED, and 2731 stores the whole config — the ACTIVE tier is lifted out
@@ -15,10 +15,11 @@
15
15
  /** The parsed DP capability catalog for one product SKU. */
16
16
  export interface DpCatalog {
17
17
  /**
18
- * For enum-type DPs: the valid integer values as declared in the catalog.
19
- * Absent for non-enum DPs (bool, raw, integer, string).
18
+ * For enum-type DPs: the range members exactly as the catalog lists them, as strings — value names
19
+ * (`"Quiet"`) or numeric strings (`"0"`), whichever the SKU's catalog uses. What a member means is
20
+ * the reading capability's to decide. Absent for non-enum DPs (bool, raw, integer, string).
20
21
  */
21
- readonly enumRanges: ReadonlyMap<number, readonly number[]>;
22
+ readonly enumRanges: ReadonlyMap<number, readonly string[]>;
22
23
  }
23
24
  /** Returned whenever the API call fails or the response shape is not recognised. */
24
25
  export declare const EMPTY_DP_CATALOG: DpCatalog;
@@ -160,6 +160,9 @@ export declare const LIGHT_MEMBERS: {
160
160
  * This is the switch a user changes and expects to STAY changed. {@link isOn} is a different fact:
161
161
  * the lamp being lit right now, driven by whichever
162
162
  * client is streaming — the vendor app lights it for a live view and drops it on quitting.
163
+ *
164
+ * The T8423 write takes the session-chosen level; level-1 int+string was verified live on a standalone
165
+ * unit.
163
166
  */
164
167
  readonly spotlightEnabled: {
165
168
  readonly param: 1403;
@@ -271,6 +271,10 @@ export declare const MOTION_MEMBERS: {
271
271
  * The raw sensitivity an indoor camera reports, on the one id whose write takes a `1700` control
272
272
  * payload carrying an index rather than the `1350` envelope its siblings use — a frame-shape
273
273
  * difference `sensitivityCommand` resolves from the scale. `unexposed` like its three siblings.
274
+ *
275
+ * Some devices report 6041 as a base64 JSON list of per-sensor entries
276
+ * (`{"list":[{"sensor_id":…,"sensitivity":…}]}`) rather than a scalar. That value is stored verbatim
277
+ * and this getter answers `undefined` for it: only the scalar form is confirmed to be this step.
274
278
  */
275
279
  readonly indoorSensitivity: {
276
280
  readonly param: 6041;
@@ -279,6 +283,7 @@ export declare const MOTION_MEMBERS: {
279
283
  readonly provenance: "verified";
280
284
  readonly unexposed: true;
281
285
  readonly description: "Raw sensitivity an indoor camera reports (verified: param 6041).";
286
+ readonly decode: (raw: unknown) => number | undefined;
282
287
  };
283
288
  /**
284
289
  * The raw sensitivity on the INVERTED seven-step camera scale — a HIGHER number is LESS sensitive, so
@@ -15,7 +15,7 @@ export declare const SUCTION_DP: {
15
15
  * Suction levels — the app's `SuctionEnum` (`getSuctionEnumByValue`). This is a **fixed** value→label
16
16
  * map with **no model argument**: a given int means the same thing on every RoboVac. What varies per
17
17
  * model is only **availability** — a device's `get_product_data_point` range may expose a narrower
18
- * subset (the T2351 catalog lists 0-3) — so the `suction` property stays a raw int rather than being
18
+ * subset (the T2351 catalog lists `Quiet`–`Max`) — so the `suction` property stays a raw int rather than being
19
19
  * constrained per device, and {@link suctionLevelName} names a reported level.
20
20
  *
21
21
  * `BoostIQ` (4) is a real suction level in this scale. The separate `boostIq`
@@ -45,11 +45,11 @@ export declare function suctionLevelName(value: number): string | undefined;
45
45
  export type SuctionActions = Surface<typeof SUCTION_MEMBERS> & {
46
46
  /**
47
47
  * The suction levels this device supports, sourced from the per-SKU `get_product_data_point` catalog
48
- * range for DP 158. Narrower than the full six {@link SuctionLevel} values on many models — a T2351
49
- * reports `[0, 1, 2, 3]`.
48
+ * range for DP 158. Narrower than the full six {@link SuctionLevel} values on many models — a T2351's
49
+ * catalog lists `Quiet`, `Standard`, `Turbo` and `Max`, which answers `[0, 1, 2, 3]`.
50
50
  *
51
- * `undefined` when the catalog is absent or does not cover DP 158. Only meaningful on devices where
52
- * `setSuctionLevel` is installed (AIoT vacuums).
51
+ * `undefined` when the catalog is absent, does not cover DP 158, or lists only members outside the
52
+ * known scale. Only meaningful on devices where `setSuctionLevel` is installed (AIoT vacuums).
53
53
  */
54
54
  readonly supportedLevels?: readonly SuctionLevelValue[];
55
55
  };
@@ -247,6 +247,12 @@ export interface AvailabilityContext {
247
247
  homeBaseAttached?: boolean;
248
248
  /** Serial of the covering station, when the device record names one. */
249
249
  stationSerial?: string;
250
+ /**
251
+ * The vendor DeviceType of the station the device hangs off, when it is attached
252
+ * and the station's record states one. Answers which station family a command addressed to an
253
+ * attached device is carried through.
254
+ */
255
+ stationDeviceType?: number;
250
256
  }
251
257
  export interface CommandContext extends AvailabilityContext {
252
258
  /** Device channel (0 for standalone, `device_channel` on a HomeBase). */
@@ -169,12 +169,11 @@ export declare const TUYA_CLEAN_TYPES: readonly ["Sweep", "SweepMop", "Mop"];
169
169
  /** @internal */
170
170
  export type TuyaCleanType = (typeof TUYA_CLEAN_TYPES)[number];
171
171
  /**
172
- * `ModeCtrlRequest.method` values for DP 152. Live-verified on T2351: START_AUTO_CLEAN → 0
173
- * (omitted from the wire when zero), START_GOHOME → 6, PAUSE_TASK → 13.
172
+ * `ModeCtrlRequest.method` values for DP 152. Live-verified on T2351: START_GOHOME → 6,
173
+ * PAUSE_TASK → 13. An auto clean is `START_AUTO_CLEAN` (0) with a payload — see
174
+ * {@link ModeCtrlParamMethod}.
174
175
  */
175
176
  export declare const ModeCtrlMethod: {
176
- /** Live-verified on a T2351. Zero, so it is omitted from the wire per the proto3 default rule. */
177
- readonly START_AUTO_CLEAN: 0;
178
177
  /** Live-verified on a T2351. */
179
178
  readonly START_GOHOME: 6;
180
179
  /** Live-verified on a T2351. */
@@ -202,9 +201,11 @@ export declare const ModeCtrlMethod: {
202
201
  * its argument in the signature: {@link VACUUM_CLEAN_MEMBERS.startScene},
203
202
  * {@link VACUUM_CLEAN_MEMBERS.cleanRooms} and {@link VACUUM_CLEAN_MEMBERS.cleanZones}.
204
203
  *
205
- * The outer frame these ride in is byte-verified on a live T2351, and `SCENE`, `SELECT_ROOMS` and
206
- * `SELECT_ZONES` have each since been RUN on a T2351 and did what they name — so their numbers rest on
207
- * observed behaviour rather than on the vendor's definition alone.
204
+ * The outer frame these ride in is byte-verified on a live T2351, and `AUTO`, `SCENE`, `SELECT_ROOMS`
205
+ * and `SELECT_ZONES` have each since been RUN on a T2351 and did what they name — so their numbers rest
206
+ * on observed behaviour rather than on the vendor's definition alone. `AUTO` is RUN with its payload:
207
+ * the vendor's definition calls `AutoClean.clean_times` valid only when non-zero, and the T2351 did
208
+ * not start on method 0 sent without it.
208
209
  *
209
210
  * That distinction is the whole point of checking, and this is the one place it is argued: an AIoT
210
211
  * data-point write is fire-and-forget, so a wrong number would be a different command arriving and
@@ -215,6 +216,11 @@ export declare const ModeCtrlMethod: {
215
216
  * scene id and a map id both arrive on DP 180.
216
217
  */
217
218
  export declare const ModeCtrlParamMethod: {
219
+ /** `START_AUTO_CLEAN` with an `AutoClean` payload — clean the whole floor. */
220
+ readonly AUTO: {
221
+ readonly method: 0;
222
+ readonly param: 3;
223
+ };
218
224
  /** `START_SELECT_ROOMS_CLEAN` — clean the named rooms of a named map. */
219
225
  readonly SELECT_ROOMS: {
220
226
  readonly method: 1;
@@ -274,17 +280,17 @@ export declare function encodeSelectZonesClean(mapId: number, zones: readonly Va
274
280
  * @internal
275
281
  */
276
282
  export declare function encodeSceneClean(sceneId: number): string;
283
+ /**
284
+ * Build a whole-floor auto clean carrying an `AutoClean` payload of one pass.
285
+ * @internal
286
+ */
287
+ export declare function encodeAutoClean(): string;
277
288
  /**
278
289
  * Encode a `ModeCtrlRequest` protobuf (DP 152) as a DP value: `varint(bodyLen) ++ {method:1, seq:2}`.
279
290
  *
280
291
  * Built on {@link RawDpWriter} rather than hand-rolled bytes. The frame is unchanged and the existing
281
292
  * byte-level test is what proves it — that test was written against a live T2351 capture, so it holds
282
293
  * the writer to the wire rather than to this function's own idea of the wire.
283
- *
284
- * Method 0 (START_AUTO_CLEAN) is omitted rather than written as an explicit zero, per the proto3
285
- * default-field rule and confirmed on that same capture. The writer deliberately does not apply that
286
- * rule itself: whether an explicit zero and an absent field mean the same thing is the
287
- * message's business, not the encoder's.
288
294
  * @internal
289
295
  */
290
296
  export declare function encodeModeCtrl(method: number, seq: number): string;
@@ -1955,7 +1961,7 @@ export declare const VACUUM_CLEAN_MEMBERS: {
1955
1961
  readonly provenance: "mega";
1956
1962
  readonly description: "Stop smart-follow mode (ModeCtrlRequest method 18 over DP 152). Method number not captured — unverified.";
1957
1963
  };
1958
- /** Start an auto-clean run via ModeCtrlRequest method 0 (DP 152). AIoT only — Tuya write unverified. */
1964
+ /** Start an auto-clean run via ModeCtrlRequest method 0 with one `AutoClean` pass (DP 152). AIoT only — Tuya write unverified. */
1959
1965
  readonly startCleaning: import("./members.js").MethodMember<() => Promise<void>> & {
1960
1966
  available: (ctx: import("./types.js").CommandContext) => boolean;
1961
1967
  };
@@ -56,8 +56,7 @@ export declare function codecForType(deviceType: number): Codec | undefined;
56
56
  * - `T74xx` → **lock** (SmartSafe 7400-series).
57
57
  * - `T80xx` / `T8001` / `T8002` / `T8010` / `T8030` / `T8023` / `T8025` → **station**
58
58
  * (HomeBase / HomeBase 2 / 3 / Mini), `T9000` (the app's own model registry files it as family
59
- * `STATION_9000`, the one station family outside the T8 band), and `T8N00` (NVR) / `T8E00` (PoE
60
- * NVR) → station.
59
+ * `STATION_9000`, the one station family outside the T8 band), and `T8N00` (NVR) → station.
61
60
  * - `T89xx` (entry/motion/water/siren sensors, e.g. T8900/T8910/T8920) → **sensor**.
62
61
  * - `T87xx` keypad (`T8960`) → **keypad**.
63
62
  * - any other `T8…` security T-code → **camera** (the default security family).
@@ -43,6 +43,10 @@ export declare const isOutdoorPanTilt: (ctx: FamilyContext) => boolean;
43
43
  export declare const isFloodLight: (ctx: FamilyContext) => boolean;
44
44
  /** HomeBase-family hub (has a speaker/alarm) — a station EXCLUDING the NVRs, per `HOMEBASE_TYPES`. */
45
45
  export declare const isHomeBase: (ctx: FamilyContext) => boolean;
46
+ /** HomeBase S1 Pro (`STATION_9000`), the station the app drives over its WebRTC control channel. */
47
+ export declare const isStation9000: (ctx: FamilyContext) => boolean;
48
+ /** T8N00 NVR, identified by its reported model. */
49
+ export declare const isNvrT8N00: (ctx: FamilyContext) => boolean;
46
50
  /** Wired doorbell (DeviceType.DOORBELL). */
47
51
  export declare const isWiredDoorbell: (ctx: FamilyContext) => boolean;
48
52
  /**
@@ -48,7 +48,8 @@ export declare function resolveDevice(rec: CloudRecord): ResolvedDevice;
48
48
  * The property manifest for a device, from its capabilities and the record's facts. The single place
49
49
  * an {@link AvailabilityContext} is built — so a family-gate (`available`) and a per-model enum
50
50
  * (`enumValuesFor`) are decided from the same truthful, session-free view on every path (initial
51
- * resolve and {@link Device.reresolve}). Populated only from what a record carries, never transport
52
- * fields a live session hasn't produced.
51
+ * resolve and {@link Device.reresolve}). Populated only from what a record carries: its cloud params and
52
+ * the params its device reported over realtime, the same evidence a bound getter is gated on. Never
53
+ * transport fields a live session hasn't produced.
53
54
  */
54
55
  export declare function resolveProperties(rec: CloudRecord, codec: Codec, capabilities: Capability[]): PropertySpec[];
@@ -286,6 +286,12 @@ export interface CloudRecord {
286
286
  parentSn?: string;
287
287
  /** Reported param_type → raw value. Presence of a param is a capability signal. */
288
288
  params?: Record<number, string>;
289
+ /**
290
+ * Params the device reported over its realtime channel, keyed in the device's own namespace. A line
291
+ * whose state exists only on that channel carries none of it in {@link params}, so this is the
292
+ * evidence an availability gate on such a param is decided from.
293
+ */
294
+ dpParams?: Record<number, string>;
289
295
  }
290
296
  /** The fully-resolved device shape produced by the 3-tier resolver. */
291
297
  export interface ResolvedDevice {
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Settle `work` as it settles, or reject the moment `signal` aborts, whichever comes first.
3
+ *
4
+ * The underlying wait is left to finish on its own: it may be shared work other callers are waiting on, so a
5
+ * caller abandoning its own call does not cancel the work itself. This abandons WAITING, which is the only
6
+ * part that belonged to the caller.
7
+ */
8
+ export declare function abortable<T>(work: Promise<T>, signal?: AbortSignal): Promise<T>;
@@ -209,6 +209,8 @@ export interface SignedRetry {
209
209
  export declare class MegaHttpClient {
210
210
  private readonly cfg;
211
211
  private region;
212
+ /** The host `estimate_domain` answered, persisted with the session. */
213
+ private estimatedDomain;
212
214
  private bootstrapDomain?;
213
215
  private sessionKey?;
214
216
  /** Per-host ECDH session keys for non-mega gateways (e.g. eufylife) keyed by host. */
@@ -275,6 +277,21 @@ export declare class MegaHttpClient {
275
277
  } | undefined;
276
278
  /** The active region shard (e.g. `"eu-pr"`, `"us-pr"`), set after {@link login} or a region override. */
277
279
  get regionShard(): RegionShard;
280
+ /**
281
+ * The shard the RTC signalling signs on: the regional prefix of the estimated domain
282
+ * (`…-ie-…`/`…-ie.` → `ie-pr`), else {@link regionShard}. The eu/us classification cannot name shards
283
+ * like `ie-pr`, whose accounts need the IE sign host and cluster.
284
+ */
285
+ get rtcShard(): string;
286
+ /**
287
+ * The credentials the RTC signalling needs; `undefined` while logged out. The sign is refused
288
+ * ("gtoken not equal userid") unless its gtoken is the one every authed HTTP call carries.
289
+ */
290
+ rtcIdentity(): {
291
+ authToken: string;
292
+ userId: string;
293
+ gtoken: string;
294
+ } | undefined;
278
295
  /**
279
296
  * The name commands attribute themselves to — {@link MegaClientConfig.accountName} when the config
280
297
  * pins one (trimmed; blank counts as unset), otherwise the logged-in account's display name, which
@@ -367,6 +384,15 @@ export declare class MegaHttpClient {
367
384
  * params instead, which also carry `{param_type, param_value, update_time}` and are not owner-gated.
368
385
  */
369
386
  getDeviceParamList<T = unknown>(deviceSn: string): Promise<T>;
387
+ /**
388
+ * Fetch the account's devices as the `devicerelation` service lists them, `{attribute: 7, house_id: ""}`.
389
+ *
390
+ * Each entry is `{device: {device_sn, dps, …}}`, and `dps` is the cloud's last-known value of every
391
+ * data point that device has reported, keyed by DP number. Answered for an account a device is shared
392
+ * with as well as for its owner. Sent with `category: eufy_home`, without which the service answers
393
+ * `10000`.
394
+ */
395
+ getDeviceRelationList<T = unknown>(): Promise<T>;
370
396
  /**
371
397
  * Fetch one page of a device's **cleaning history** from the mega `clean` service.
372
398
  *
@@ -1,14 +1,3 @@
1
- /**
2
- * P2P command router — the transport-side owner of the ThroughTek PPCS sessions and every wire
3
- * operation over them: opening sessions, resolving a serial to its session + routing params,
4
- * mapping a transport-neutral {@link Command} to a concrete frame (encryption level, wire shape),
5
- * the fire-and-forget control senders, request/reply queries, and the media provider.
6
- *
7
- * Layering: this module knows P2P bytes; it does NOT know capabilities. Frame → semantic-event
8
- * decoding is a model concern, so raw frames are handed back to the client via {@link P2PRouterDeps.onFrame}
9
- * (the client gates them on device capabilities and emits typed events). This keeps transport free of
10
- * any `model/` import — the capability↔transport decorrelation invariant.
11
- */
12
1
  import type { MegaHttpClient } from "../http/mega-client.js";
13
2
  import type { EufyDevice } from "../../core/types.js";
14
3
  import type { Command, AutoLockSnapshot, MediaProvider, SharedSourceHints, AbortableCall } from "../../core/contracts.js";
@@ -258,11 +247,14 @@ export declare class P2PCommandRouter {
258
247
  */
259
248
  dispatchCommand(sn: string, cmd: Command): Promise<void>;
260
249
  /**
261
- * Restart a HomeBase. `RESTART_HUB` (1034) is a station-scalar on the broadcast channel 255: a
262
- * level-2 frame whose body is `[u32 value][account_id padded]` — the same shape as the hub
263
- * alarm-volume control. ✅ Wire-confirmed byte-exact from a capture of the app's own Restart
264
- * (2026-08-03) and HW-tested: the captured frame carried value `0` and rebooted the hub. Replays
265
- * like every other level-2 control, so a single dropped datagram doesn't lose it.
250
+ * Restart the station that owns `sn`'s P2P session. `RESTART_HUB` (1034) is a station-scalar on the
251
+ * broadcast channel 255 whose body is `[u32 value][account_id padded]`, value `0`. ✅ Wire-confirmed
252
+ * byte-exact as a level-2 frame from a capture of the app's own HomeBase Restart (2026-08-03).
253
+ *
254
+ * The seal follows the session, as {@link sendBySessionLevel} defines it: a keyed session takes the
255
+ * level-2 frame, a keyless one — a standalone camera, which never negotiates a key — the same body
256
+ * sealed level-1. Both are replayed {@link DIRECT_CMD_SENDS}× at 200ms, as every other unacknowledged
257
+ * control here is.
266
258
  */
267
259
  rebootStation(sn: string): Promise<void>;
268
260
  /**
@@ -308,6 +300,11 @@ export declare class P2PCommandRouter {
308
300
  * stream costs nothing extra and a talkback on an otherwise idle camera opens the session it needs
309
301
  * instead of playing into silence.
310
302
  *
303
+ * **On a model in {@link ELD_TALKBACK_MODELS}, the speaker plays the codec the camera sends.** So the first
304
+ * audio frame of the media session this talkback holds decides its codec ({@link speakerCodec}), within
305
+ * {@link TALKBACK_CODEC_WAIT_MS} of the session's first frame, and the claim is re-checked after that wait as
306
+ * after the warm-up. Every other model opens `aac-lc` at once.
307
+ *
311
308
  * The level-2 key is waited for softly: only the HomeBase-attached path requires it, and
312
309
  * {@link Talkback.start} reports that failure precisely, so a hard wait here would reject an
313
310
  * own-session camera that legitimately never negotiates one.
@@ -453,15 +450,6 @@ export declare class P2PCommandRouter {
453
450
  * A still with no live sibling re-asserts, so a tile refreshing on a quiet station is
454
451
  * unaffected.
455
452
  */
456
- /**
457
- * Attach a consumer, unless the caller has already abandoned the call.
458
- *
459
- * The acquisition it just waited through can outlast the caller's interest, and a consumer attached for
460
- * somebody who has gone keeps the pull warm for nobody. Detaching immediately gives the pull back, which
461
- * lets it linger and fall away if this was the only thing holding it, and leaves it untouched if it was
462
- * not.
463
- */
464
- private attachUnlessAborted;
465
453
  /**
466
454
  * Dispose one cached live source and forget it, so the next acquisition builds a fresh one. Its claim
467
455
  * on a session goes with it, and a media session opened for this source alone is closed: nothing else
@@ -687,7 +675,7 @@ export declare class P2PCommandRouter {
687
675
  * **`set-json-raw` intent** — bare JSON, no envelope: outer P2P cmd = `outerCmd` itself, plaintext
688
676
  * exactly `{account_id,...data}` (`session.sendRawLevel2` with no wrapper). Reversed from a live
689
677
  * capture of the app's own SET_SNOOZE_TIME (1271) frame — see `param-dictionary.ts`'s `1271` entry
690
- * (`snoozeTime`); the alarm-delay config (1255, `arming.ts`'s `ARMING_CMD.ALARM_DELAY_CONFIG`) reuses
678
+ * (`snoozeTime`); the alarm-delay config (1255, `arming.ts`'s `ARMING_CMD.SET_ALL_ACTION`) reuses
691
679
  * the same bare-JSON shape.
692
680
  */
693
681
  private sendJsonRaw;
@@ -20,6 +20,8 @@ export declare class Fmp4Muxer {
20
20
  private lastVideoTimestampMs?;
21
21
  private readonly audioRequested;
22
22
  private audioCodec?;
23
+ /** Decoder config of an AAC-ELD track, taken from its frames; absent for AAC-LC, whose `esds` config is fixed. */
24
+ private audioConfig?;
23
25
  private audioDisabled;
24
26
  private audioSamples;
25
27
  private audioBaseDecodeTime;
@@ -37,7 +39,10 @@ export declare class Fmp4Muxer {
37
39
  * (e.g. delta frames before the first keyframe).
38
40
  */
39
41
  push(frame: LiveVideoFrame, timestampMs?: number): MediaFragment | undefined;
40
- /** Add one station-declared AAC access unit to the audio track. G.711 remains available via `live()`. */
42
+ /**
43
+ * Add one station-declared AAC access unit to the audio track: an ADTS frame for AAC-LC, a raw access unit
44
+ * described by its frame's config for AAC-ELD. G.711 remains available via `live()`.
45
+ */
41
46
  pushAudio(frame: LiveAudioFrame, timestampMs?: number): MediaFragment | undefined;
42
47
  /** Permanently omit incompatible audio while allowing the video recording to continue. */
43
48
  private disableAudio;
@@ -103,6 +103,9 @@ export declare class LiveStream extends EventEmitter {
103
103
  private lastCodec;
104
104
  /** Rebuilds an access unit the station split across several frames — see {@link AccessUnitAssembler}. */
105
105
  private readonly units;
106
+ /** Whether a missing video datagram or incomplete unit invalidated the current reference chain. */
107
+ private awaitingKeyframeAfterGap;
108
+ private readonly videoGapHandler;
106
109
  /** The channel inbound media must be tagged with, once {@link acceptsMedia} trusts the station's tag. */
107
110
  private mediaChannel?;
108
111
  private tracedFirstVideoCommand;
@@ -277,8 +277,8 @@ export declare class P2PSession extends EventEmitter {
277
277
  private decryptLevel2;
278
278
  get isConnected(): boolean;
279
279
  /**
280
- * Ask the OS for {@link RECEIVE_BUFFER_BYTES} on a bound socket, and warn once per session when it grants
281
- * less or refuses.
280
+ * Ask the OS for {@link RECEIVE_BUFFER_BYTES} on a bound socket, and log once per session when it grants
281
+ * less (debug) or refuses (warn).
282
282
  *
283
283
  * The request is made here rather than through `createSocket`'s `recvBufferSize`: Node applies that option
284
284
  * inside the bind callback, where a refusal is thrown out of reach of this session and ends the process.
@@ -435,6 +435,11 @@ export declare class P2PSession extends EventEmitter {
435
435
  * "direct" commands (e.g. CAMERA_SWITCH 1035) carry a binary struct, not JSON.
436
436
  */
437
437
  sendRawLevel2Bytes(payload: Buffer, channel: number, outerCmd?: number, signCode?: number): boolean;
438
+ /**
439
+ * The level-1 twin of {@link sendRawLevel2Bytes}: the plaintext `payload` sealed AES-128-ECB under the
440
+ * level-1 key, signCode 1 — the seal of the level-1 media start. Returns `false` when not connected.
441
+ */
442
+ sendRawLevel1Bytes(payload: Buffer, channel: number, outerCmd: number): boolean;
438
443
  /**
439
444
  * Stop the realtime media stream (`CMD_STOP_REALTIME_MEDIA`, 1004) on a camera `channel`, in the shape
440
445
  * its runtime topology takes, as {@link startLiveMedia} selects its start:
@@ -606,6 +611,18 @@ export declare class P2PSession extends EventEmitter {
606
611
  accountId?: string;
607
612
  channel?: number;
608
613
  }): void;
614
+ /**
615
+ * Read a date-ranged page of `history_record_info` from the station. The reply's `data` array groups
616
+ * recording rows with companion tables; each group retains its `table_name` and `payload`.
617
+ * `startTime` is `"0"` for the newest page or the oldest row's `yyyyMMddHHmmss` cursor for older rows.
618
+ */
619
+ queryRecordPage(opts: {
620
+ accountId?: string;
621
+ startDate: string;
622
+ endDate: string;
623
+ startTime?: string;
624
+ count?: number;
625
+ }): Promise<unknown[]>;
609
626
  /**
610
627
  * Query one on-station table and answer its rows, once the reply is whole.
611
628
  *
@@ -613,13 +630,15 @@ export declare class P2PSession extends EventEmitter {
613
630
  * several frames whose decrypted text is a fragment of one document, so the fragments are
614
631
  * accumulated here and scanned after each one. Answers that document's `data` rows. Rejects when
615
632
  * `signal` aborts, and when `timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}) elapses with no
616
- * complete reply.
633
+ * complete reply, and when the station answers a non-zero `mIntRet`.
617
634
  *
618
635
  * One read at a time per session: while one is accumulating, every {@link queryDatabase} on the
619
636
  * session throws, so a second reply cannot land in this buffer.
620
637
  */
621
638
  readDatabase(table: string, opts?: {
622
639
  accountId?: string;
640
+ innerCmd?: number;
641
+ query?: Record<string, unknown>;
623
642
  timeoutMs?: number;
624
643
  signal?: AbortSignal;
625
644
  }): Promise<unknown[]>;
@@ -46,9 +46,10 @@ export declare function receiveRecording(session: P2PSession, request: {
46
46
  signal?: AbortSignal;
47
47
  }): RecordingTransfer;
48
48
  /**
49
- * Decode a recording's frames, in arrival order, into elementary streams. Keyframes go through one
50
- * {@link VideoFrameDecoder}, whose media key then opens the audio; a keyframe that does not open under
51
- * `eccPrivateKeyHex`, and every audio frame before the first media key, are dropped. Rejects with
49
+ * Decode a recording's frames, in arrival order, into elementary streams. Split pictures are assembled
50
+ * before they enter the output; an orphan continuation cannot become its own picture. Keyframes go
51
+ * through one {@link VideoFrameDecoder}, whose media key then opens the audio; a keyframe that does not
52
+ * open under `eccPrivateKeyHex`, and every audio frame before the first media key, are dropped. Rejects with
52
53
  * {@link RecordingDownloadError} `undecodable` when no video frame decodes.
53
54
  */
54
55
  export declare function decodeRecording(frames: readonly RecordingFrame[], eccPrivateKeyHex: string): RecordingDownload;
@@ -438,3 +438,12 @@ export declare class SharedLiveSource {
438
438
  */
439
439
  dispose(): void;
440
440
  }
441
+ /**
442
+ * Attach a consumer, unless the caller has already abandoned the call.
443
+ *
444
+ * The acquisition it just waited through can outlast the caller's interest, and a consumer attached for
445
+ * somebody who has gone keeps the pull warm for nobody. Detaching immediately gives the pull back, which
446
+ * lets it linger and fall away if this was the only thing holding it, and leaves it untouched if it was
447
+ * not.
448
+ */
449
+ export declare function attachUnlessAborted(source: SharedLiveSource, signal?: AbortSignal): Consumer;