@mega-yfue/eufy-sdk 0.4.0-beta.8 → 0.4.0
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.
- package/dist/client/eufy-mega.d.ts +18 -2
- package/dist/client/types.d.ts +25 -0
- package/dist/core/contracts.d.ts +43 -0
- package/dist/index.js +1065 -548
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/camera.d.ts +14 -9
- package/dist/model/capabilities/doorbell.d.ts +8 -9
- package/dist/model/capabilities/index.d.ts +1 -1
- package/dist/model/capabilities/manifest.d.ts +18 -2
- package/dist/model/capabilities/members.d.ts +2 -0
- package/dist/model/capabilities/motion.d.ts +3 -4
- package/dist/model/capabilities/siren.d.ts +1 -1
- package/dist/model/capabilities/types.d.ts +2 -0
- package/dist/model/param-dictionary.d.ts +3 -3
- package/dist/model/types.d.ts +10 -11
- package/dist/transport/http/passport.d.ts +21 -0
- package/dist/transport/http/solix-client.d.ts +5 -6
- package/dist/transport/p2p/codec.d.ts +13 -0
- package/dist/transport/p2p/command-router.d.ts +36 -6
- package/dist/transport/p2p/live-trace.d.ts +5 -0
- package/dist/transport/p2p/p2p-session.d.ts +51 -14
- package/dist/transport/p2p/recording-download.d.ts +54 -0
- package/dist/transport/p2p/video.d.ts +2 -0
- package/dist/transport/stored-image-cache.d.ts +2 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Surface } from "./members.js";
|
|
2
|
-
import type { CapabilityModule, CommandContext } from "./types.js";
|
|
2
|
+
import type { AvailabilityContext, CapabilityModule, CommandContext } from "./types.js";
|
|
3
3
|
import { type Command, type MediaProvider } from "../../core/contracts.js";
|
|
4
4
|
/**
|
|
5
5
|
* The P2P **feature-command ids** this camera capability drives (direct-binary switches + `1350`
|
|
@@ -8,8 +8,10 @@ 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 on/off.
|
|
11
|
+
/** Camera on/off. Read and write polarity depend on the family and topology. */
|
|
12
12
|
readonly CAMERA_ENABLE: 1035;
|
|
13
|
+
/** Wrapped T8410 power switch on HomeBase 3. */
|
|
14
|
+
readonly PRIVACY_MODE: 6250;
|
|
13
15
|
/**
|
|
14
16
|
* Camera status LED on/off — the "power/recording" indicator. The
|
|
15
17
|
* app JS has a sibling param `1056` (`APP_CMD_LIVEVIEW_LED_SWITCH`) for the SAME UI "Status Light"
|
|
@@ -270,17 +272,17 @@ export type CameraActions = Surface<typeof CAMERA_MEMBERS> & {
|
|
|
270
272
|
*/
|
|
271
273
|
export declare const CAMERA_MEMBERS: {
|
|
272
274
|
/**
|
|
273
|
-
* The READ is the *disable*-bit convention (1035 "0" ⇒ ON, 2001 direct); the WRITE polarity is
|
|
275
|
+
* The default READ is the *disable*-bit convention (1035 "0" ⇒ ON, 2001 direct); the WRITE polarity is
|
|
274
276
|
* family-dependent — see `powerValue` / `isEnableBitPolarity`. Battery/solo cams report the state under
|
|
275
277
|
* 1035, standalone indoor/outdoor cams under 2001 OPEN_DEVICE with direct polarity, so 2001 is a
|
|
276
278
|
* read-alias. Both verified live, and the write polarity is confirmed against the app's own frames.
|
|
277
279
|
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
+
* T8410 on HomeBase 3 reports 1035 as an enable bit ("1" ⇒ ON). That reported state does not reliably
|
|
281
|
+
* reflect the privacy command. `readReflectsWrite` states this distinction.
|
|
280
282
|
*
|
|
281
283
|
* The privacy param (6250) is reported by the outdoor-PT family and by no other camera measured, and both
|
|
282
284
|
* of its polarities are observed. It is deliberately NOT aliased here: it moved in the same step as 1035, so
|
|
283
|
-
* the reading cannot say whether power and privacy are one state or two
|
|
285
|
+
* the reading cannot say whether power and privacy are one state or two — so
|
|
284
286
|
* aliasing a second param could only fold two possible states into one getter for no gain.
|
|
285
287
|
*/
|
|
286
288
|
readonly enabled: {
|
|
@@ -289,10 +291,12 @@ export declare const CAMERA_MEMBERS: {
|
|
|
289
291
|
readonly kind: "boolean";
|
|
290
292
|
readonly provenance: "verified";
|
|
291
293
|
readonly invert: true;
|
|
294
|
+
readonly invertFor: (ctx: AvailabilityContext) => false | undefined;
|
|
292
295
|
readonly readAliases: readonly [{
|
|
293
296
|
readonly paramType: 2001;
|
|
294
297
|
readonly invert: false;
|
|
295
298
|
}];
|
|
299
|
+
readonly readReflectsWrite: (ctx: CommandContext) => boolean;
|
|
296
300
|
readonly description: string;
|
|
297
301
|
readonly observation: {
|
|
298
302
|
readonly event: "cameraEnabledChanged";
|
|
@@ -512,10 +516,10 @@ export declare const CAMERA_MEMBERS: {
|
|
|
512
516
|
readonly type: "bool";
|
|
513
517
|
readonly kind: "boolean";
|
|
514
518
|
readonly provenance: "verified";
|
|
515
|
-
readonly readAvailable: (ctx:
|
|
519
|
+
readonly readAvailable: (ctx: AvailabilityContext) => boolean;
|
|
516
520
|
readonly readAliases: readonly [{
|
|
517
521
|
readonly paramType: 1716;
|
|
518
|
-
readonly available: (ctx:
|
|
522
|
+
readonly available: (ctx: AvailabilityContext) => boolean;
|
|
519
523
|
}];
|
|
520
524
|
readonly requiresRead: true;
|
|
521
525
|
readonly description: "Camera status LED. Video doorbells report the same state under their button-ring LED parameter.";
|
|
@@ -526,7 +530,7 @@ export declare const CAMERA_MEMBERS: {
|
|
|
526
530
|
* value, and it exists only on a device bound to a provider. Each is declared once, with its
|
|
527
531
|
* signature taken FROM {@link MediaProvider} — so a change there is a compile error here, not a drift.
|
|
528
532
|
*
|
|
529
|
-
* Every pull is refused where {@link CAMERA_MEMBERS.enabled} reads false, with {@link CameraDisabledError} —
|
|
533
|
+
* Every pull is refused where a reliable {@link CAMERA_MEMBERS.enabled} reads false, with {@link CameraDisabledError} —
|
|
530
534
|
* `live`, `snapshotLive`, `record`, `openReadable` and `recordFragments` alike, since each opens media on a
|
|
531
535
|
* camera that serves none. That reading is the on/off source; a live probe is not one, since a disabled
|
|
532
536
|
* camera answers a start with audio and never a video frame.
|
|
@@ -552,6 +556,7 @@ export declare const CAMERA_MEMBERS: {
|
|
|
552
556
|
} | undefined) => Promise<Buffer<ArrayBufferLike>>>;
|
|
553
557
|
readonly openReadable: import("./members.js").ProvidedMember<"media", ((opts?: Parameters<NonNullable<MediaProvider["openReadable"]>>[0]) => Promise<import("node:stream").Readable>) | undefined>;
|
|
554
558
|
readonly recordFragments: import("./members.js").ProvidedMember<"media", ((opts?: Parameters<NonNullable<MediaProvider["recordFragments"]>>[0]) => import("../../core/contracts.js").FragmentRecordingHandle) | undefined>;
|
|
559
|
+
readonly downloadRecording: import("./members.js").ProvidedMember<"media", ((opts: Parameters<NonNullable<MediaProvider["downloadRecording"]>>[0]) => Promise<import("../../core/contracts.js").RecordingDownload>) | undefined>;
|
|
555
560
|
/**
|
|
556
561
|
* Push audio from the host to this camera's speaker. Gated on the **speaker** param specifically —
|
|
557
562
|
* not the `audio` capability, which resolves on a microphone alone and would advertise a speaker the
|
|
@@ -164,7 +164,7 @@ export declare function parseQuickResponses(voiceList: Array<{
|
|
|
164
164
|
}>): QuickResponse[];
|
|
165
165
|
/**
|
|
166
166
|
* `doorbell` — chime / ringtone configuration. CONFIRMED against a real Video Doorbell (T8214):
|
|
167
|
-
* the live ids are the `1702-1719` `CMD_BAT_DOORBELL_*` range
|
|
167
|
+
* the live ids are the `1702-1719` `CMD_BAT_DOORBELL_*` range, observed on that device. The
|
|
168
168
|
* legacy `2015/2022/1306` ids are excluded — they appear on NO owned device. The button-
|
|
169
169
|
* press *event* (ring) is delivered out-of-band via `CMD_DOORBELL_NOTIFY_PAYLOAD` (1701) /
|
|
170
170
|
* push/MQTT — it is handled by the Phase-1 event normalizers, not as a device-list param.
|
|
@@ -185,9 +185,9 @@ export declare function parseQuickResponses(voiceList: Array<{
|
|
|
185
185
|
export declare const DOORBELL_MEMBERS: {
|
|
186
186
|
/**
|
|
187
187
|
* The HOMEBASE as the doorbell's chime — the hub plays the ring, not the wired chime box
|
|
188
|
-
* `mechanicalChimeSwitch` drives. `provenance` is "verified" on our own decrypt of the app's frame
|
|
189
|
-
*
|
|
190
|
-
*
|
|
188
|
+
* `mechanicalChimeSwitch` drives. `provenance` is "verified" on our own decrypt of the app's frame:
|
|
189
|
+
* direct-binary `[ch][value][acct]`, 1=on/0=off, the same shape as its 1703 sibling — captured, not
|
|
190
|
+
* inferred from the shared param range.
|
|
191
191
|
*/
|
|
192
192
|
readonly chimeSwitch: {
|
|
193
193
|
readonly param: 1702;
|
|
@@ -198,9 +198,8 @@ export declare const DOORBELL_MEMBERS: {
|
|
|
198
198
|
readonly write: (v: string | number | boolean, ctx: import("./types.js").CommandContext) => import("../../core/contracts.js").Command;
|
|
199
199
|
};
|
|
200
200
|
/**
|
|
201
|
-
* `provenance` is "verified"
|
|
202
|
-
*
|
|
203
|
-
* `[ch][value][acct]`, 1=on/0=off — verified live on a T8214 (ON then OFF).
|
|
201
|
+
* `provenance` is "verified" on our own P2P decrypt of the write wire: direct-binary
|
|
202
|
+
* `[ch][value][acct]`, 1=on/0=off, verified live on a T8214 (ON then OFF).
|
|
204
203
|
*/
|
|
205
204
|
readonly mechanicalChimeSwitch: {
|
|
206
205
|
readonly param: 1703;
|
|
@@ -231,7 +230,7 @@ export declare const DOORBELL_MEMBERS: {
|
|
|
231
230
|
readonly type: "number";
|
|
232
231
|
readonly unit: "%";
|
|
233
232
|
readonly kind: "percent";
|
|
234
|
-
readonly provenance: "
|
|
233
|
+
readonly provenance: "verified";
|
|
235
234
|
readonly writtenElsewhere: true;
|
|
236
235
|
readonly description: string;
|
|
237
236
|
};
|
|
@@ -325,7 +324,7 @@ export declare const DOORBELL_MEMBERS: {
|
|
|
325
324
|
readonly param: 1710;
|
|
326
325
|
readonly type: "string";
|
|
327
326
|
readonly kind: "text";
|
|
328
|
-
readonly provenance: "
|
|
327
|
+
readonly provenance: "verified";
|
|
329
328
|
readonly description: string;
|
|
330
329
|
};
|
|
331
330
|
/**
|
|
@@ -19,7 +19,7 @@ export { SirenVolume, type SirenVolumeValue, SirenAlarmDuration, type SirenAlarm
|
|
|
19
19
|
*/
|
|
20
20
|
export { unobservableMembers, unreflectedMembers } from "./members.js";
|
|
21
21
|
export { Watermark, type WatermarkValue, NightVision, type NightVisionValue, RecordingQuality, type RecordingQualityName, RECORDING_QUALITY_TIERS, StreamingQuality, type StreamingQualityName, STREAMING_QUALITY_TIERS, resolveStreamingQuality, resolveStreamingQualityTier, resolveRecordingQuality, resolveRecordingQualityValue, resolveRecordingQualityTier, } from "./camera.js";
|
|
22
|
-
export { WorkingMode, PowerSource, resolveWorkingMode, resolveWorkingModeValue, WORKING_MODE_MAPS, type WorkingModeName, type PowerSourceName, } from "./battery.js";
|
|
22
|
+
export { WorkingMode, PowerSource, resolveWorkingMode, resolveWorkingModeValue, cameraPowerTier, WORKING_MODE_MAPS, type WorkingModeName, type PowerSourceName, } from "./battery.js";
|
|
23
23
|
export { type DpCatalog, EMPTY_DP_CATALOG } from "./dp-catalog.js";
|
|
24
24
|
import type { PtzActions } from "./ptz.js";
|
|
25
25
|
import type { LightActions } from "./light.js";
|
|
@@ -61,8 +61,14 @@ export interface ActionDescriptor extends ActionSpec {
|
|
|
61
61
|
/** What one capability exposes on a device — the join of its bound object and its own declaration. */
|
|
62
62
|
export interface CapabilityDescriptor {
|
|
63
63
|
capability: Capability;
|
|
64
|
-
/**
|
|
65
|
-
|
|
64
|
+
/**
|
|
65
|
+
* The fluent accessor this capability is reached under: `dev[accessor]()`.
|
|
66
|
+
*
|
|
67
|
+
* Absent for a capability with nothing to bind — one whose whole surface is inbound events, so it
|
|
68
|
+
* declares no members and no `actions()` and therefore has no object to reach. Such a capability is
|
|
69
|
+
* described for its {@link events} alone; every other field is empty.
|
|
70
|
+
*/
|
|
71
|
+
accessor?: string;
|
|
66
72
|
/** The reads INSTALLED on this device, never the theoretical set. */
|
|
67
73
|
reads: readonly ReadDescriptor[];
|
|
68
74
|
/** The installed actions that carry a description. */
|
|
@@ -102,6 +108,16 @@ export interface DeviceManifest {
|
|
|
102
108
|
* Parameterised over the module list; the barrel binds it to the real one. A capability the device did
|
|
103
109
|
* not bind — because it does not have it, or because nothing is bound yet — contributes no descriptor
|
|
104
110
|
* at all.
|
|
111
|
+
*
|
|
112
|
+
* With ONE exception, and it is not a bound object: a capability whose whole surface is inbound events
|
|
113
|
+
* declares no members and no `actions()`, so `buildActions` builds nothing for it and there is no
|
|
114
|
+
* object here to walk. Its events are still device truth, and the resolved set in {@link
|
|
115
|
+
* AvailabilityContext.capabilities} is what says this device has it — so it is described from its own
|
|
116
|
+
* declaration, with no {@link CapabilityDescriptor.accessor} and every other field empty, and only on
|
|
117
|
+
* a device with at least one bound object.
|
|
118
|
+
*
|
|
119
|
+
* Claims resolve against an empty read set there, which is the truthful evidence — a module that binds
|
|
120
|
+
* nothing installs no getter, so a `reads` claim cannot hold. A topology claim still applies.
|
|
105
121
|
* @internal
|
|
106
122
|
*/
|
|
107
123
|
export declare function describeBound(modules: readonly CapabilityModule[], bound: Readonly<Record<string, unknown>>, ctx?: AvailabilityContext): CapabilityDescriptor[];
|
|
@@ -48,6 +48,8 @@ export interface ValueMember {
|
|
|
48
48
|
enumValuesFor?: (ctx: AvailabilityContext) => Record<number, string> | undefined;
|
|
49
49
|
provenance?: PropertySpec["provenance"];
|
|
50
50
|
invert?: boolean;
|
|
51
|
+
/** Per-device read polarity; undefined retains the static default. */
|
|
52
|
+
invertFor?: (ctx: AvailabilityContext) => boolean | undefined;
|
|
51
53
|
description: string;
|
|
52
54
|
/** The wire, or absent for read-only. `undefined` from it = this value is not one we accept. */
|
|
53
55
|
write?: (value: boolean | number | string, ctx: CommandContext) => Command | undefined;
|
|
@@ -98,7 +98,6 @@ export declare const MOTION_CMD: {
|
|
|
98
98
|
* payload key AND pass mChannel 0 explicitly; the two are separate choices, not linked.)
|
|
99
99
|
*
|
|
100
100
|
* ⚠️ Replay + readback confirmed on a HomeBase-attached T8425 (1719 `0`→`1`→`0`), NOT byte-captured.
|
|
101
|
-
* Provenance is `apk`, not `verified`: a divergent-but-also-accepted frame can't be ruled out.
|
|
102
101
|
*/
|
|
103
102
|
readonly HUMAN_ONLY_AT_NIGHT: 1719;
|
|
104
103
|
/**
|
|
@@ -112,7 +111,7 @@ export declare const MOTION_CMD: {
|
|
|
112
111
|
* form, so a feature that is on reads as off.
|
|
113
112
|
* Decoded by {@link decodeRadarWdSwitch} to match the app.
|
|
114
113
|
*
|
|
115
|
-
* ⚠️ Replay + readback confirmed on a T8214 (2706 `0`→`1`→`0`), NOT byte-captured.
|
|
114
|
+
* ⚠️ Replay + readback confirmed on a T8214 (2706 `0`→`1`→`0`), NOT byte-captured.
|
|
116
115
|
*/
|
|
117
116
|
readonly LOITERING_DETECTION: 2706;
|
|
118
117
|
/**
|
|
@@ -354,7 +353,7 @@ export declare const MOTION_MEMBERS: {
|
|
|
354
353
|
readonly param: 1719;
|
|
355
354
|
readonly type: "bool";
|
|
356
355
|
readonly kind: "boolean";
|
|
357
|
-
readonly provenance: "
|
|
356
|
+
readonly provenance: "verified";
|
|
358
357
|
readonly description: string;
|
|
359
358
|
readonly requires: readonly [1719];
|
|
360
359
|
readonly write: (v: string | number | boolean, ctx: CommandContext) => Command;
|
|
@@ -367,7 +366,7 @@ export declare const MOTION_MEMBERS: {
|
|
|
367
366
|
readonly param: 2706;
|
|
368
367
|
readonly type: "bool";
|
|
369
368
|
readonly kind: "boolean";
|
|
370
|
-
readonly provenance: "
|
|
369
|
+
readonly provenance: "verified";
|
|
371
370
|
readonly coerce: (raw: string | number | boolean) => boolean;
|
|
372
371
|
readonly description: string;
|
|
373
372
|
readonly requires: readonly [2706];
|
|
@@ -27,7 +27,7 @@ export declare const SIREN_CMD: {
|
|
|
27
27
|
readonly ALARM_TEST: 1826;
|
|
28
28
|
/** Manually stop a sounding alarm (app `APP_CMD_SIREN_SENSOR_MANUAL_STOP_ALARM`). */
|
|
29
29
|
readonly MANUAL_STOP: 1871;
|
|
30
|
-
/** Station-family HomeBase duration alarm, verified live on T8010 (app `SET_TONE_FILE`). */
|
|
30
|
+
/** Station-family HomeBase duration alarm, verified live on T8010 and T8030 (app `SET_TONE_FILE`). */
|
|
31
31
|
readonly HOMEBASE_TONE: 1201;
|
|
32
32
|
/** HomeBase alarm volume percentage (app `CMD_SET_HUB_SPK_VOLUME`). */
|
|
33
33
|
readonly HUB_SPK_VOLUME: 1235;
|
|
@@ -245,6 +245,8 @@ export interface AvailabilityContext {
|
|
|
245
245
|
* cannot do what its name promises there.
|
|
246
246
|
*/
|
|
247
247
|
homeBaseAttached?: boolean;
|
|
248
|
+
/** Serial of the covering station, when the device record names one. */
|
|
249
|
+
stationSerial?: string;
|
|
248
250
|
}
|
|
249
251
|
export interface CommandContext extends AvailabilityContext {
|
|
250
252
|
/** Device channel (0 for standalone, `device_channel` on a HomeBase). */
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* Two namespaces (params are per-transport, NOT globally unique):
|
|
6
6
|
* - SECURITY_PARAMS — eufy P2P param space (ids 1000+). **Membership is the observation**: an id is
|
|
7
7
|
* listed only because the sweep saw it on a real owned device, so the id is real/accepted.
|
|
8
|
-
* `provenance` is the trust of the NAME/meaning
|
|
9
|
-
*
|
|
8
|
+
* `provenance` is the trust of the NAME/meaning; the tiers are defined on `PropertySource` in
|
|
9
|
+
* `types.ts`.
|
|
10
10
|
* - CLEAN_PARAMS — RoboVac Tuya DP space (ids 1 and above), names from the cloud
|
|
11
|
-
* `get_product_data_point` data_point_list
|
|
11
|
+
* `get_product_data_point` data_point_list, hence provenance `mega`.
|
|
12
12
|
*
|
|
13
13
|
* Which models reported an id, and the capture that named it, are in the commit that adds the entry.
|
|
14
14
|
*/
|
package/dist/model/types.d.ts
CHANGED
|
@@ -104,18 +104,17 @@ export type ValueKind = KnownValueKind | (string & {});
|
|
|
104
104
|
*/
|
|
105
105
|
export declare function isKnownValueKind(kind: ValueKind): kind is KnownValueKind;
|
|
106
106
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* - `
|
|
110
|
-
*
|
|
111
|
-
* - `
|
|
107
|
+
* How much a param's name and meaning can be trusted, most-trusted first. This is the one definition;
|
|
108
|
+
* the param dictionary and every `provenance` field use it.
|
|
109
|
+
* - `mega` — the vendor's cloud names it: its data-point catalog (`get_product_data_point`), or a
|
|
110
|
+
* reported value that matches what the cloud record already says (a model name or code).
|
|
111
|
+
* - `verified` — confirmed on a real device by this project: our own capture or observation, or
|
|
112
|
+
* identified by someone who has the hardware.
|
|
113
|
+
* - `apk` — the v6 app's own decompiled constant name, not yet confirmed on a device.
|
|
114
|
+
* - `guessed` — no name source; a plausible placeholder until a toggle-diff settles it.
|
|
112
115
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* own decompiled code (`apk`) or our own capture/observation (`verified`), never someone else's
|
|
116
|
-
* unverified guess. Absent provenance is treated as `guessed`.
|
|
117
|
-
*
|
|
118
|
-
* Provenance of a property definition — an internal trust label used when curating the model.
|
|
116
|
+
* A third-party reverse-engineering project is never a source: every name we ship comes from the
|
|
117
|
+
* vendor's cloud, our own observation or the app itself. Absent provenance is treated as `guessed`.
|
|
119
118
|
* @internal
|
|
120
119
|
*/
|
|
121
120
|
export type PropertySource = "mega" | "apk" | "verified" | "guessed";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `algo_ecdh` passport steps the eufy and Solix clients share byte for byte. Hosts, header sets, key
|
|
3
|
+
* caching, 2FA delivery and the `gtoken` id differ per app line and stay in each client.
|
|
4
|
+
*/
|
|
5
|
+
import { type SessionEntry } from "../../core/index.js";
|
|
6
|
+
/** The credential fields every `/passport/login` body starts with; each client adds its own challenge fields. */
|
|
7
|
+
export declare function loginCredentials(email: string, password: string, country: string): Record<string, unknown>;
|
|
8
|
+
/**
|
|
9
|
+
* Read a decrypted `/passport/login` reply; `undefined` when it carries no id or no token. `twoFactorPending`
|
|
10
|
+
* is true while `fa_info.info` is non-empty and false once 2FA is satisfied.
|
|
11
|
+
*/
|
|
12
|
+
export declare function readLoginReply(data: Record<string, unknown>): {
|
|
13
|
+
userId: string;
|
|
14
|
+
accountUserId: string | undefined;
|
|
15
|
+
authToken: string;
|
|
16
|
+
geoKey: string | undefined;
|
|
17
|
+
tokenExpiresAt: number;
|
|
18
|
+
twoFactorPending: boolean;
|
|
19
|
+
} | undefined;
|
|
20
|
+
/** The headers that mark `encBody` as `algo_ecdh`-encrypted under `entry` and sign it. */
|
|
21
|
+
export declare function signedHeaders(entry: SessionEntry, encBody: string): Record<string, string>;
|
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
* eufy client uses.
|
|
4
4
|
*
|
|
5
5
|
* Why this is separate from the eufy device client: Solix shares Anker's `algo_ecdh` passport (so
|
|
6
|
-
* {@link prepareKeyExchange}
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* and
|
|
10
|
-
*
|
|
11
|
-
* the encrypted passport handshake to obtain a token, then makes plain authenticated reads.
|
|
6
|
+
* {@link prepareKeyExchange} and the steps in `./passport.ts` are shared with it) but exposes a different device
|
|
7
|
+
* backend — its own `app-name`, host, and bootstrap key (`SOLIX_APP_NAME`, `SOLIX_DEFAULT_API_HOST`,
|
|
8
|
+
* {@link SOLIX_LOCAL_KEY_HEX}) — and its authenticated resource reads are PLAIN JSON, carrying only the auth token
|
|
9
|
+
* and a `gtoken = md5(user_id)`, with no per-request encryption or signature. This client therefore does the
|
|
10
|
+
* encrypted passport handshake to obtain a token, then makes plain authenticated reads.
|
|
12
11
|
*
|
|
13
12
|
* This is the wire client (transport layer): it returns the vendor's typed JSON as received. Building
|
|
14
13
|
* those records into capability-driven `SolixDevice` models is the model layer's job — see
|
|
@@ -22,6 +22,11 @@ export declare const ResponseMessageType: {
|
|
|
22
22
|
readonly LOOKUP_ADDR: Buffer<ArrayBuffer>;
|
|
23
23
|
readonly LOOKUP_ADDR2: Buffer<ArrayBuffer>;
|
|
24
24
|
readonly CAM_ID: Buffer<ArrayBuffer>;
|
|
25
|
+
/**
|
|
26
|
+
* The device's own address record, sent in answer to CHECK_CAM ahead of CAM_ID: its 20-byte device id, one
|
|
27
|
+
* record in `encodeSelfAddress`'s shape naming the address it answers from, then 8 zero bytes (44-byte payload).
|
|
28
|
+
*/
|
|
29
|
+
readonly CAM_ADDR: Buffer<ArrayBuffer>;
|
|
25
30
|
readonly TURN_SERVER_CAM_ID: Buffer<ArrayBuffer>;
|
|
26
31
|
readonly PING: Buffer<ArrayBuffer>;
|
|
27
32
|
readonly PONG: Buffer<ArrayBuffer>;
|
|
@@ -149,6 +154,14 @@ export declare function buildStringCommandPayload(value: string, channel?: numbe
|
|
|
149
154
|
* length prefix ({@link stringWithLength}).
|
|
150
155
|
*/
|
|
151
156
|
export declare function buildIntStringCommandPayload(value: number, valueSub: number, strValue: string, channel?: number, key?: Buffer, encType?: number): Buffer;
|
|
157
|
+
/**
|
|
158
|
+
* Build a **string-pair** command body: five zero bytes, then `strValue` and `strValueSub`, each in the
|
|
159
|
+
* 128-byte-chunk length form ({@link stringWithLength}), AES-128-ECB encrypted (level-1) like
|
|
160
|
+
* {@link buildStringCommandPayload} when `key` is given. `CMD_DOWNLOAD_VIDEO` (1024) takes this shape on a
|
|
161
|
+
* HomeBase 2: `strValue` = the recording's path on the station, `strValueSub` = the station admin
|
|
162
|
+
* `account_id`, on the camera's channel.
|
|
163
|
+
*/
|
|
164
|
+
export declare function buildStringPairCommandPayload(strValue: string, strValueSub: string, channel?: number, key?: Buffer, encType?: number): Buffer;
|
|
152
165
|
/**
|
|
153
166
|
* Build a command body around an ALREADY-encrypted (or plaintext) `data` buffer with an explicit
|
|
154
167
|
* `signCode` — used for level-2 (`signCode 8`, AES-256-GCM) commands like the media-start 1350 the
|
|
@@ -121,6 +121,8 @@ export declare class P2PCommandRouter {
|
|
|
121
121
|
private readonly talkbacks;
|
|
122
122
|
/** cipher_id → ECC private key (one eufylife get_ciphers call per cipher), shared across (re)opens. */
|
|
123
123
|
private readonly cipherKeyCache;
|
|
124
|
+
/** The recording download in flight per station; a station serves one at a time. */
|
|
125
|
+
private readonly recordingDownloads;
|
|
124
126
|
constructor(deps: P2PRouterDeps);
|
|
125
127
|
/** Forward one P2P failure once even when both the session listener and startup waiter observe it. */
|
|
126
128
|
private reportError;
|
|
@@ -237,6 +239,12 @@ export declare class P2PCommandRouter {
|
|
|
237
239
|
* while its own session is still serving.
|
|
238
240
|
*/
|
|
239
241
|
private makeSession;
|
|
242
|
+
/**
|
|
243
|
+
* The ECC private key of `cipherId`, from the router's cache or one `get_ciphers` call. When the cloud
|
|
244
|
+
* answers with another cipher, that one is used and traced on `session`. Only a successful lookup is
|
|
245
|
+
* cached.
|
|
246
|
+
*/
|
|
247
|
+
private cipherKeyFor;
|
|
240
248
|
/**
|
|
241
249
|
* Open (or reuse) a station's P2P session and await its completed handshake. An optional abort only
|
|
242
250
|
* stops this wait; session ownership remains with {@link SessionManager} and its normal teardown.
|
|
@@ -269,6 +277,27 @@ export declare class P2PCommandRouter {
|
|
|
269
277
|
* start has no level-1 form for.
|
|
270
278
|
*/
|
|
271
279
|
mediaProviderFor(sn: string): MediaProvider;
|
|
280
|
+
/**
|
|
281
|
+
* Whether `sn` is a camera attached to a HomeBase 2 (T8010), the one station whose recording path layout
|
|
282
|
+
* and frame formats are confirmed.
|
|
283
|
+
*/
|
|
284
|
+
private downloadsRecordings;
|
|
285
|
+
/**
|
|
286
|
+
* Download one recording a HomeBase 2 holds for an attached camera, then decode it
|
|
287
|
+
* ({@link decodeRecording}). Downloads queue per station: the station streams one recording at a time
|
|
288
|
+
* on the camera's channel, so a download starts only once the previous one has drained
|
|
289
|
+
* ({@link RecordingTransfer.drained}), even when that one's caller already has its answer. An abort
|
|
290
|
+
* while waiting rejects without sending anything; an abort mid-transfer rejects at once and the station's
|
|
291
|
+
* turn is held until the transfer drains.
|
|
292
|
+
*/
|
|
293
|
+
private downloadRecording;
|
|
294
|
+
/** Resolve once `previous` settles, or reject as soon as `signal` aborts. */
|
|
295
|
+
private waitTurn;
|
|
296
|
+
/**
|
|
297
|
+
* Start one recording download, once the station's previous download has drained: the decoded recording,
|
|
298
|
+
* and when the station is done sending it.
|
|
299
|
+
*/
|
|
300
|
+
private startRecordingDownload;
|
|
272
301
|
/**
|
|
273
302
|
* Open a {@link Talkback} on a device's camera channel.
|
|
274
303
|
*
|
|
@@ -461,9 +490,9 @@ export declare class P2PCommandRouter {
|
|
|
461
490
|
*
|
|
462
491
|
* Reached two ways, both idempotent: the session's own `close` event, when it died while still the
|
|
463
492
|
* station's registered session, and {@link SessionManagerOpts.onAutoClose}, when the manager closed it
|
|
464
|
-
* unasked. A close a CALLER made is deliberately not routed here — {@link closeAll}
|
|
465
|
-
* sources first,
|
|
466
|
-
* on the replacement session.
|
|
493
|
+
* unasked. A close a CALLER made is deliberately not routed here — {@link closeAll} and
|
|
494
|
+
* {@link resolveSession} dispose their own sources first, while {@link replaceUnreachableSession} keeps
|
|
495
|
+
* its source alive on purpose to rewarm it on the replacement session.
|
|
467
496
|
*/
|
|
468
497
|
private tearDownStation;
|
|
469
498
|
/**
|
|
@@ -621,9 +650,10 @@ export declare class P2PCommandRouter {
|
|
|
621
650
|
*
|
|
622
651
|
* A session whose {@link P2PSession.pathAnswering} is false is closed and re-resolved before it is handed
|
|
623
652
|
* over: the station answers every heartbeat, so a path silent past several of them is gone. A session
|
|
624
|
-
* reporting nothing about its path is not reporting that evidence and is handed over as it is.
|
|
625
|
-
*
|
|
626
|
-
*
|
|
653
|
+
* reporting nothing about its path is not reporting that evidence and is handed over as it is. Cached live
|
|
654
|
+
* sources on the replaced session are discarded before closing it (see {@link tearDownStation}).
|
|
655
|
+
* Replaced at most once per resolution, so a station whose replacement is silent too is returned rather
|
|
656
|
+
* than closed again.
|
|
627
657
|
*/
|
|
628
658
|
private resolveSession;
|
|
629
659
|
/**
|
|
@@ -71,11 +71,16 @@ export type LiveTrace =
|
|
|
71
71
|
* an address to ask. A connect that had one channel failed for that channel's reason alone, and a connect
|
|
72
72
|
* that had neither could not have succeeded — outcomes a station that is switched off is otherwise
|
|
73
73
|
* indistinguishable from, because nothing else in a failed connect states what was even attempted.
|
|
74
|
+
*
|
|
75
|
+
* `cloudMissing` names the input the cloud channel lacked, present exactly when `cloud` is false: `dsk-key`
|
|
76
|
+
* where no key was held for the station, `cloud-addresses` where its record named no address to ask. The key
|
|
77
|
+
* is named where both are absent.
|
|
74
78
|
*/
|
|
75
79
|
| {
|
|
76
80
|
phase: "lookup-channels";
|
|
77
81
|
local: boolean;
|
|
78
82
|
cloud: boolean;
|
|
83
|
+
cloudMissing?: "dsk-key" | "cloud-addresses";
|
|
79
84
|
}
|
|
80
85
|
/**
|
|
81
86
|
* Work on a station is holding for its session to connect, with the milliseconds it will wait.
|
|
@@ -123,8 +123,8 @@ export declare class P2PSession extends EventEmitter {
|
|
|
123
123
|
private audioStalled;
|
|
124
124
|
private audioRetransmitTimer?;
|
|
125
125
|
private lastPongData?;
|
|
126
|
-
/** When
|
|
127
|
-
private
|
|
126
|
+
/** When the selected peer last answered outbound P2P traffic on this connection. */
|
|
127
|
+
private lastPeerAt?;
|
|
128
128
|
/** Whether the silence has already been stated, so it is traced once per connection rather than per read. */
|
|
129
129
|
private pathStaleTraced;
|
|
130
130
|
private lookupTimer?;
|
|
@@ -187,15 +187,15 @@ export declare class P2PSession extends EventEmitter {
|
|
|
187
187
|
/**
|
|
188
188
|
* How long this connection's path has been silent, or nothing where it has never answered.
|
|
189
189
|
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
190
|
+
* PONG and ACK from the selected peer prove the path answers outbound traffic. `undefined` means no such reply
|
|
191
|
+
* has arrived since connection, so silence alone does not establish a stale path.
|
|
192
192
|
*/
|
|
193
193
|
get pathSilentMs(): number | undefined;
|
|
194
194
|
/**
|
|
195
195
|
* Whether this path can still be committed to, on the evidence the heartbeat gives.
|
|
196
196
|
*
|
|
197
|
-
* False where
|
|
198
|
-
* not known to be dead, so it answers true.
|
|
197
|
+
* False where selected-peer replies arrived and then stopped for {@link PATH_SILENCE_MS}. A connection with
|
|
198
|
+
* no post-connect reply is not known to be dead, so it answers true.
|
|
199
199
|
*
|
|
200
200
|
* Traces the silence once per connection, on the read that first observes it.
|
|
201
201
|
*/
|
|
@@ -326,6 +326,9 @@ export declare class P2PSession extends EventEmitter {
|
|
|
326
326
|
* straddling a level-2 wait on a camera whose failure to deliver video had a separate cause, and did not
|
|
327
327
|
* recur across later probes of it. Correlate an unmodelled type against a WORKING session before reading it
|
|
328
328
|
* as a cause.
|
|
329
|
+
*
|
|
330
|
+
* CAM_ADDR is recognised and dropped: it names the address the device is already answering from, and the
|
|
331
|
+
* CAM_ID that follows it is what completes the connect.
|
|
329
332
|
*/
|
|
330
333
|
private onMessage;
|
|
331
334
|
private beginCheckCam;
|
|
@@ -410,6 +413,8 @@ export declare class P2PSession extends EventEmitter {
|
|
|
410
413
|
* `strValue` (admin `account_id`). See {@link buildIntStringCommandPayload}. Fire-and-forget.
|
|
411
414
|
*/
|
|
412
415
|
sendIntStringCommand(commandType: number, value: number, valueSub: number, strValue: string, channel?: number): void;
|
|
416
|
+
/** Send a level-1 {@link buildStringPairCommandPayload} command on `channel`. */
|
|
417
|
+
sendStringPairCommand(commandType: number, strValue: string, strValueSub: string, channel: number): void;
|
|
413
418
|
/**
|
|
414
419
|
* Send a **level-2 (AES-256-GCM, signCode 8) control payload** to a HomeBase-attached device. The
|
|
415
420
|
* target camera is selected by `channel` (= device_channel) + the `mChannel` envelope — the same
|
|
@@ -430,8 +435,17 @@ export declare class P2PSession extends EventEmitter {
|
|
|
430
435
|
* "direct" commands (e.g. CAMERA_SWITCH 1035) carry a binary struct, not JSON.
|
|
431
436
|
*/
|
|
432
437
|
sendRawLevel2Bytes(payload: Buffer, channel: number, outerCmd?: number, signCode?: number): boolean;
|
|
433
|
-
/**
|
|
434
|
-
|
|
438
|
+
/**
|
|
439
|
+
* Stop the realtime media stream (`CMD_STOP_REALTIME_MEDIA`, 1004) on a camera `channel`, in the shape
|
|
440
|
+
* its runtime topology takes, as {@link startLiveMedia} selects its start:
|
|
441
|
+
* - `homeBaseAttached`: `CMD_SET_PAYLOAD` (1350) wrapping `{cmd:1004, mChannel:channel}` at level-2.
|
|
442
|
+
* - own-session at level-2: the direct 1004 frame whose entire plaintext is the camera channel as a
|
|
443
|
+
* `uint32`, the same 4-byte body as the attached talkback frames. The app sends this frame on closing
|
|
444
|
+
* an own-session camera's live view; the camera stops streaming on it within a second, and keeps
|
|
445
|
+
* streaming through the 1350-wrapped form.
|
|
446
|
+
* - without a level-2 key: the bare 1004 command.
|
|
447
|
+
*/
|
|
448
|
+
stopLiveMedia(channel?: number, accountId?: string, homeBaseAttached?: boolean): void;
|
|
435
449
|
/**
|
|
436
450
|
* Open the device's talkback (host→device audio) path on a camera `channel`, after which
|
|
437
451
|
* {@link sendAudioFrame} is accepted until {@link stopTalkback}. Two protocols, selected by runtime
|
|
@@ -510,11 +524,6 @@ export declare class P2PSession extends EventEmitter {
|
|
|
510
524
|
* camera is selected by `mChannel` (= the device's `device_channel`) AND the frame-header channel.
|
|
511
525
|
*/
|
|
512
526
|
private sendMediaPayloadLevel2;
|
|
513
|
-
/**
|
|
514
|
-
* Encrypt a level-2 command body (signCode 8): `tag(16) ‖ nonce(12) ‖ [seq,03,02,01](4) ‖
|
|
515
|
-
* ciphertext`, AES-256-GCM under the negotiated session key, AAD "eufy security". Inverse of
|
|
516
|
-
* `decryptLevel2`. The 4-byte sub-header is cleartext (skipped on decrypt); `seq` is a counter.
|
|
517
|
-
*/
|
|
518
527
|
/**
|
|
519
528
|
* Decode a `CMD_VIDEO_FRAME` (1300) payload into clean Annex-B H.264 (the 22-byte frame header
|
|
520
529
|
* stripped). Reversed from the V6 app + live H.264 captures: the 22-byte header is
|
|
@@ -538,6 +547,11 @@ export declare class P2PSession extends EventEmitter {
|
|
|
538
547
|
* key. Lazily generates a keypair; `rsaPrivateKey` decrypts the media key the station returns.
|
|
539
548
|
*/
|
|
540
549
|
private rsaModulus;
|
|
550
|
+
/**
|
|
551
|
+
* Encrypt a level-2 command body (signCode 8): `tag(16) ‖ nonce(12) ‖ seq(4) ‖ ciphertext`,
|
|
552
|
+
* AES-256-GCM under the negotiated session key, AAD "eufy security". Inverse of `decryptLevel2`.
|
|
553
|
+
* The 4-byte sub-header is cleartext (skipped on decrypt); `seq` counts up from {@link LEVEL2_SEQ_BASE}.
|
|
554
|
+
*/
|
|
541
555
|
private encryptLevel2;
|
|
542
556
|
/** Send a no-arg command frame (e.g. CMD_GATEWAYINFO) on a channel (default: the station channel). */
|
|
543
557
|
private sendCommand;
|
|
@@ -561,6 +575,10 @@ export declare class P2PSession extends EventEmitter {
|
|
|
561
575
|
* The HomeBase streams back `CMD_DATABASE` (1306) frames `{cmd:10000,count,data:[…]}`,
|
|
562
576
|
* level-1-encrypted — decoded and emitted as `dbChunk` (decrypted text) per frame.
|
|
563
577
|
* Tables: `familiar_faces`, `person_basic_info`, `event_person_list`, `history_record_info`.
|
|
578
|
+
*
|
|
579
|
+
* Throws while a {@link readDatabase} is accumulating: every table answers `{data:[…]}` and the
|
|
580
|
+
* frames tie no chunk to its request, so a second query's reply would be assembled into the first
|
|
581
|
+
* one's buffer and answered as its rows.
|
|
564
582
|
*/
|
|
565
583
|
queryDatabase(table: string, opts?: {
|
|
566
584
|
accountId?: string;
|
|
@@ -588,6 +606,25 @@ export declare class P2PSession extends EventEmitter {
|
|
|
588
606
|
accountId?: string;
|
|
589
607
|
channel?: number;
|
|
590
608
|
}): void;
|
|
609
|
+
/**
|
|
610
|
+
* Query one on-station table and answer its rows, once the reply is whole.
|
|
611
|
+
*
|
|
612
|
+
* The request half of {@link queryDatabase} with its reply assembled: `CMD_DATABASE` arrives as
|
|
613
|
+
* several frames whose decrypted text is a fragment of one document, so the fragments are
|
|
614
|
+
* accumulated here and scanned after each one. Answers that document's `data` rows. Rejects when
|
|
615
|
+
* `signal` aborts, and when `timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}) elapses with no
|
|
616
|
+
* complete reply.
|
|
617
|
+
*
|
|
618
|
+
* One read at a time per session: while one is accumulating, every {@link queryDatabase} on the
|
|
619
|
+
* session throws, so a second reply cannot land in this buffer.
|
|
620
|
+
*/
|
|
621
|
+
readDatabase(table: string, opts?: {
|
|
622
|
+
accountId?: string;
|
|
623
|
+
timeoutMs?: number;
|
|
624
|
+
signal?: AbortSignal;
|
|
625
|
+
}): Promise<unknown[]>;
|
|
626
|
+
/** Whether a {@link readDatabase} is accumulating; see {@link queryDatabase} for what it bars. */
|
|
627
|
+
private dbReadInFlight;
|
|
591
628
|
/**
|
|
592
629
|
* Request the **face feature rows** over P2P (`face_feature_info`, inner `cmd 10000`). Each row
|
|
593
630
|
* carries `{person_id, face_name, face_id, face_picture_content, face_feature_file_path}` — where
|
|
@@ -640,7 +677,7 @@ export declare class P2PSession extends EventEmitter {
|
|
|
640
677
|
* if the missing one arrives. A logical frame's payload spans datagrams that carry no header of their
|
|
641
678
|
* own, so reassembling around a hole is impossible; waiting for it is what keeps the frame whole.
|
|
642
679
|
*
|
|
643
|
-
* Only once
|
|
680
|
+
* Only once the data type's reorder wait passes, or {@link REORDER_MAX_DATAGRAMS} pile up, is the datagram
|
|
644
681
|
* treated as lost: a pending frame is discarded and delivery resumes from the earliest held datagram.
|
|
645
682
|
*/
|
|
646
683
|
private onData;
|