@mega-yfue/eufy-sdk 0.4.0-beta.2 → 0.4.0-beta.21
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 +22 -3
- package/dist/client/types.d.ts +25 -0
- package/dist/core/contracts.d.ts +45 -2
- package/dist/index.js +1204 -681
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/camera.d.ts +1 -0
- package/dist/model/capabilities/doorbell.d.ts +8 -9
- package/dist/model/capabilities/manifest.d.ts +18 -2
- package/dist/model/capabilities/motion.d.ts +3 -4
- package/dist/model/capabilities/siren.d.ts +1 -1
- 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/mqtt/secure-mqtt.d.ts +20 -13
- package/dist/transport/p2p/adts.d.ts +6 -0
- package/dist/transport/p2p/codec.d.ts +13 -0
- package/dist/transport/p2p/command-router.d.ts +29 -0
- package/dist/transport/p2p/p2p-session.d.ts +60 -13
- package/dist/transport/p2p/recording-download.d.ts +54 -0
- package/dist/transport/p2p/shared-live-source.d.ts +1 -1
- package/dist/transport/p2p/video.d.ts +2 -0
- package/dist/transport/stored-image-cache.d.ts +2 -0
- package/package.json +1 -1
|
@@ -552,6 +552,7 @@ export declare const CAMERA_MEMBERS: {
|
|
|
552
552
|
} | undefined) => Promise<Buffer<ArrayBufferLike>>>;
|
|
553
553
|
readonly openReadable: import("./members.js").ProvidedMember<"media", ((opts?: Parameters<NonNullable<MediaProvider["openReadable"]>>[0]) => Promise<import("node:stream").Readable>) | undefined>;
|
|
554
554
|
readonly recordFragments: import("./members.js").ProvidedMember<"media", ((opts?: Parameters<NonNullable<MediaProvider["recordFragments"]>>[0]) => import("../../core/contracts.js").FragmentRecordingHandle) | undefined>;
|
|
555
|
+
readonly downloadRecording: import("./members.js").ProvidedMember<"media", ((opts: Parameters<NonNullable<MediaProvider["downloadRecording"]>>[0]) => Promise<import("../../core/contracts.js").RecordingDownload>) | undefined>;
|
|
555
556
|
/**
|
|
556
557
|
* Push audio from the host to this camera's speaker. Gated on the **speaker** param specifically —
|
|
557
558
|
* 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
|
/**
|
|
@@ -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[];
|
|
@@ -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;
|
|
@@ -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
|
|
@@ -99,28 +99,35 @@ export declare class SecureMqtt extends EventEmitter implements RealtimeTranspor
|
|
|
99
99
|
private open;
|
|
100
100
|
/**
|
|
101
101
|
* Subscribe every inbound leg this device's line uses (see `topics.ts` — one `/res` for most lines,
|
|
102
|
-
* four topics for `eufy_life`).
|
|
102
|
+
* four topics for `eufy_life` and the clean line).
|
|
103
103
|
*
|
|
104
|
-
* The grants are INSPECTED, not assumed: AWS IoT
|
|
105
|
-
* SUBACK_FAILURE
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* line that grants its state channel but refuses (say) the OTA leg still works.
|
|
104
|
+
* The grants are INSPECTED, not assumed: AWS IoT refuses a policy-denied filter with a
|
|
105
|
+
* {@link SUBACK_FAILURE} grant rather than failing the connection. A denied topic is reported via
|
|
106
|
+
* `error` naming the credential scope; only an all-denied device throws, so a line that grants its
|
|
107
|
+
* state channel but refuses (say) a business leg still works.
|
|
109
108
|
*/
|
|
110
109
|
subscribeDevice(device: EufyDevice): Promise<void>;
|
|
111
110
|
/**
|
|
112
111
|
* Subscribe to explicit topic filters, returning the topics that were granted. A scope-denied filter
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
112
|
+
* is dropped from the result instead of throwing — callers that need every leg check the returned
|
|
113
|
+
* list. Used by lines whose topic vocabulary isn't the eufy `subscribeTopics` shape (e.g. Anker Solix
|
|
114
|
+
* `dt/{app}/{pn}/{sn}`).
|
|
116
115
|
*/
|
|
117
116
|
subscribe(topics: string[]): Promise<string[]>;
|
|
118
117
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
118
|
+
* Subscribe each filter in a SUBSCRIBE of its own and split the outcome into granted vs refused.
|
|
119
|
+
*
|
|
120
|
+
* One request per filter because the MQTT engine treats a SUBACK as all-or-nothing: one refused grant
|
|
121
|
+
* rejects the whole request, and it forgets EVERY filter of that request for resubscription after a
|
|
122
|
+
* reconnect — so a batch holding one denied leg would lose the granted legs on the next drop. Alone,
|
|
123
|
+
* a refused filter costs only itself. A rejection that carries no SUBACK is a transport failure and
|
|
124
|
+
* is thrown.
|
|
125
|
+
*
|
|
126
|
+
* The cost is one SUBSCRIBE and one SUBACK per filter instead of one per device — four for a
|
|
127
|
+
* four-leg line. They are sent concurrently, so the wall-clock cost is one round trip. Folding them
|
|
128
|
+
* back into one request brings back the lost resubscription.
|
|
122
129
|
*/
|
|
123
|
-
private
|
|
130
|
+
private subscribeEach;
|
|
124
131
|
/**
|
|
125
132
|
* Publish a raw payload to an MQTT topic (the command leg — `cmd/{app}/{pn}/{sn}/req`). The `body`
|
|
126
133
|
* is a pre-built envelope the caller supplies (the command router builds it). QoS 1 by default (the
|
|
@@ -59,6 +59,12 @@ export interface AdtsHeader {
|
|
|
59
59
|
* nothing about whether the frame's payload has arrived yet; that is the scanner's job.
|
|
60
60
|
*/
|
|
61
61
|
export declare function parseAdtsHeader(buf: Buffer, offset?: number): AdtsHeader | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* The 7-byte ADTS header (no CRC, buffer fullness "variable") that frames one raw AAC-LC, 16 kHz, mono
|
|
64
|
+
* access unit of `payloadLength` bytes, so that header and payload together read back through
|
|
65
|
+
* {@link parseAdtsHeader} as a supported frame.
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildAdtsHeader(payloadLength: number): Buffer;
|
|
62
68
|
/**
|
|
63
69
|
* Whether a header describes the audio parameters the device's path is fixed at — AAC-LC, 16 kHz,
|
|
64
70
|
* mono. A stream at any other rate or channel count is rejected rather than resampled: the device has
|
|
@@ -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
|
*
|
|
@@ -95,6 +95,8 @@ export declare class P2PSession extends EventEmitter {
|
|
|
95
95
|
private connected;
|
|
96
96
|
private connecting;
|
|
97
97
|
private closed;
|
|
98
|
+
/** Whether a short receive buffer has been reported, so a reconnect does not repeat the same warning. */
|
|
99
|
+
private receiveBufferReported;
|
|
98
100
|
private connectAddress?;
|
|
99
101
|
private seqNumber;
|
|
100
102
|
/**
|
|
@@ -121,8 +123,8 @@ export declare class P2PSession extends EventEmitter {
|
|
|
121
123
|
private audioStalled;
|
|
122
124
|
private audioRetransmitTimer?;
|
|
123
125
|
private lastPongData?;
|
|
124
|
-
/** When
|
|
125
|
-
private
|
|
126
|
+
/** When the selected peer last answered outbound P2P traffic on this connection. */
|
|
127
|
+
private lastPeerAt?;
|
|
126
128
|
/** Whether the silence has already been stated, so it is traced once per connection rather than per read. */
|
|
127
129
|
private pathStaleTraced;
|
|
128
130
|
private lookupTimer?;
|
|
@@ -134,7 +136,10 @@ export declare class P2PSession extends EventEmitter {
|
|
|
134
136
|
private cloudLookup?;
|
|
135
137
|
/** Local outbound IPv4 reported inside LOOKUP_WITH_KEY requests. */
|
|
136
138
|
private selfHost?;
|
|
137
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* In-flight multi-datagram frame per data channel (see reassemble): the payload gathered so far under its
|
|
141
|
+
* parsed header, or, without a header, the start of a frame header cut by the datagram boundary.
|
|
142
|
+
*/
|
|
138
143
|
private readonly pendingByDataType;
|
|
139
144
|
/** Last delivered datagram sequence number per data type. */
|
|
140
145
|
private readonly lastSeqByType;
|
|
@@ -182,15 +187,15 @@ export declare class P2PSession extends EventEmitter {
|
|
|
182
187
|
/**
|
|
183
188
|
* How long this connection's path has been silent, or nothing where it has never answered.
|
|
184
189
|
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
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.
|
|
187
192
|
*/
|
|
188
193
|
get pathSilentMs(): number | undefined;
|
|
189
194
|
/**
|
|
190
195
|
* Whether this path can still be committed to, on the evidence the heartbeat gives.
|
|
191
196
|
*
|
|
192
|
-
* False where
|
|
193
|
-
* 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.
|
|
194
199
|
*
|
|
195
200
|
* Traces the silence once per connection, on the read that first observes it.
|
|
196
201
|
*/
|
|
@@ -271,6 +276,14 @@ export declare class P2PSession extends EventEmitter {
|
|
|
271
276
|
*/
|
|
272
277
|
private decryptLevel2;
|
|
273
278
|
get isConnected(): boolean;
|
|
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.
|
|
282
|
+
*
|
|
283
|
+
* The request is made here rather than through `createSocket`'s `recvBufferSize`: Node applies that option
|
|
284
|
+
* inside the bind callback, where a refusal is thrown out of reach of this session and ends the process.
|
|
285
|
+
*/
|
|
286
|
+
private requestReceiveBuffer;
|
|
274
287
|
/** Open the socket and start the lookup → hole-punch handshake. */
|
|
275
288
|
connect(): Promise<void>;
|
|
276
289
|
/** Cached across every `P2PSession` in this process — the local outbound IPv4 doesn't vary by
|
|
@@ -313,6 +326,9 @@ export declare class P2PSession extends EventEmitter {
|
|
|
313
326
|
* straddling a level-2 wait on a camera whose failure to deliver video had a separate cause, and did not
|
|
314
327
|
* recur across later probes of it. Correlate an unmodelled type against a WORKING session before reading it
|
|
315
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.
|
|
316
332
|
*/
|
|
317
333
|
private onMessage;
|
|
318
334
|
private beginCheckCam;
|
|
@@ -397,6 +413,8 @@ export declare class P2PSession extends EventEmitter {
|
|
|
397
413
|
* `strValue` (admin `account_id`). See {@link buildIntStringCommandPayload}. Fire-and-forget.
|
|
398
414
|
*/
|
|
399
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;
|
|
400
418
|
/**
|
|
401
419
|
* Send a **level-2 (AES-256-GCM, signCode 8) control payload** to a HomeBase-attached device. The
|
|
402
420
|
* target camera is selected by `channel` (= device_channel) + the `mChannel` envelope — the same
|
|
@@ -497,11 +515,6 @@ export declare class P2PSession extends EventEmitter {
|
|
|
497
515
|
* camera is selected by `mChannel` (= the device's `device_channel`) AND the frame-header channel.
|
|
498
516
|
*/
|
|
499
517
|
private sendMediaPayloadLevel2;
|
|
500
|
-
/**
|
|
501
|
-
* Encrypt a level-2 command body (signCode 8): `tag(16) ‖ nonce(12) ‖ [seq,03,02,01](4) ‖
|
|
502
|
-
* ciphertext`, AES-256-GCM under the negotiated session key, AAD "eufy security". Inverse of
|
|
503
|
-
* `decryptLevel2`. The 4-byte sub-header is cleartext (skipped on decrypt); `seq` is a counter.
|
|
504
|
-
*/
|
|
505
518
|
/**
|
|
506
519
|
* Decode a `CMD_VIDEO_FRAME` (1300) payload into clean Annex-B H.264 (the 22-byte frame header
|
|
507
520
|
* stripped). Reversed from the V6 app + live H.264 captures: the 22-byte header is
|
|
@@ -525,6 +538,11 @@ export declare class P2PSession extends EventEmitter {
|
|
|
525
538
|
* key. Lazily generates a keypair; `rsaPrivateKey` decrypts the media key the station returns.
|
|
526
539
|
*/
|
|
527
540
|
private rsaModulus;
|
|
541
|
+
/**
|
|
542
|
+
* Encrypt a level-2 command body (signCode 8): `tag(16) ‖ nonce(12) ‖ seq(4) ‖ ciphertext`,
|
|
543
|
+
* AES-256-GCM under the negotiated session key, AAD "eufy security". Inverse of `decryptLevel2`.
|
|
544
|
+
* The 4-byte sub-header is cleartext (skipped on decrypt); `seq` counts up from {@link LEVEL2_SEQ_BASE}.
|
|
545
|
+
*/
|
|
528
546
|
private encryptLevel2;
|
|
529
547
|
/** Send a no-arg command frame (e.g. CMD_GATEWAYINFO) on a channel (default: the station channel). */
|
|
530
548
|
private sendCommand;
|
|
@@ -548,6 +566,10 @@ export declare class P2PSession extends EventEmitter {
|
|
|
548
566
|
* The HomeBase streams back `CMD_DATABASE` (1306) frames `{cmd:10000,count,data:[…]}`,
|
|
549
567
|
* level-1-encrypted — decoded and emitted as `dbChunk` (decrypted text) per frame.
|
|
550
568
|
* Tables: `familiar_faces`, `person_basic_info`, `event_person_list`, `history_record_info`.
|
|
569
|
+
*
|
|
570
|
+
* Throws while a {@link readDatabase} is accumulating: every table answers `{data:[…]}` and the
|
|
571
|
+
* frames tie no chunk to its request, so a second query's reply would be assembled into the first
|
|
572
|
+
* one's buffer and answered as its rows.
|
|
551
573
|
*/
|
|
552
574
|
queryDatabase(table: string, opts?: {
|
|
553
575
|
accountId?: string;
|
|
@@ -575,6 +597,25 @@ export declare class P2PSession extends EventEmitter {
|
|
|
575
597
|
accountId?: string;
|
|
576
598
|
channel?: number;
|
|
577
599
|
}): void;
|
|
600
|
+
/**
|
|
601
|
+
* Query one on-station table and answer its rows, once the reply is whole.
|
|
602
|
+
*
|
|
603
|
+
* The request half of {@link queryDatabase} with its reply assembled: `CMD_DATABASE` arrives as
|
|
604
|
+
* several frames whose decrypted text is a fragment of one document, so the fragments are
|
|
605
|
+
* accumulated here and scanned after each one. Answers that document's `data` rows. Rejects when
|
|
606
|
+
* `signal` aborts, and when `timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}) elapses with no
|
|
607
|
+
* complete reply.
|
|
608
|
+
*
|
|
609
|
+
* One read at a time per session: while one is accumulating, every {@link queryDatabase} on the
|
|
610
|
+
* session throws, so a second reply cannot land in this buffer.
|
|
611
|
+
*/
|
|
612
|
+
readDatabase(table: string, opts?: {
|
|
613
|
+
accountId?: string;
|
|
614
|
+
timeoutMs?: number;
|
|
615
|
+
signal?: AbortSignal;
|
|
616
|
+
}): Promise<unknown[]>;
|
|
617
|
+
/** Whether a {@link readDatabase} is accumulating; see {@link queryDatabase} for what it bars. */
|
|
618
|
+
private dbReadInFlight;
|
|
578
619
|
/**
|
|
579
620
|
* Request the **face feature rows** over P2P (`face_feature_info`, inner `cmd 10000`). Each row
|
|
580
621
|
* carries `{person_id, face_name, face_id, face_picture_content, face_feature_file_path}` — where
|
|
@@ -643,7 +684,13 @@ export declare class P2PSession extends EventEmitter {
|
|
|
643
684
|
*/
|
|
644
685
|
private abandonHole;
|
|
645
686
|
private clearReorderTimer;
|
|
646
|
-
/**
|
|
687
|
+
/**
|
|
688
|
+
* Reassemble one in-sequence datagram body into logical frames.
|
|
689
|
+
*
|
|
690
|
+
* Frames are packed back to back, so a frame header can itself be cut by a datagram boundary. The start
|
|
691
|
+
* of a header left at the end of a datagram is carried into the next one rather than discarded; dropping
|
|
692
|
+
* it would lose that frame and every frame after it until a datagram happened to begin on a header.
|
|
693
|
+
*/
|
|
647
694
|
private reassemble;
|
|
648
695
|
/**
|
|
649
696
|
* Forget where each data type's sequence numbering had reached, and drop any half-reassembled frame.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type RecordingDownload } from "../../core/contracts.js";
|
|
2
|
+
import type { P2PSession } from "./p2p-session.js";
|
|
3
|
+
/** Data type a station sends a recording's frames on: the binary channel (`0xd1 0x03`), not live video's. */
|
|
4
|
+
export declare const RECORDING_DATA_TYPE = 3;
|
|
5
|
+
/**
|
|
6
|
+
* Most frame bytes a transfer holds. The station sends faster than real time, so the time bounds alone do
|
|
7
|
+
* not bound memory; an event recording is far below this.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MAX_TRANSFER_BYTES: number;
|
|
10
|
+
/** One recording frame as received, in arrival order. */
|
|
11
|
+
export interface RecordingFrame {
|
|
12
|
+
commandId: number;
|
|
13
|
+
signCode: number;
|
|
14
|
+
raw: Buffer;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The path a HomeBase 2 stores a camera's recording under: the camera's channel, two digits, and the
|
|
18
|
+
* recording name the event push carries. Answers `undefined` for a name that is not the station's
|
|
19
|
+
* fourteen-digit `yyyyMMddHHmmss` form. The name comes from a push, so the test stays anchored and
|
|
20
|
+
* digits-only: no `..`, slash or other character that could reach another path on the station gets through.
|
|
21
|
+
*/
|
|
22
|
+
export declare function homeBase2RecordingPath(channel: number, recording: string): string | undefined;
|
|
23
|
+
/** One recording transfer on a station session. */
|
|
24
|
+
export interface RecordingTransfer {
|
|
25
|
+
/** The recording's frames, in arrival order. Rejects as {@link receiveRecording} describes. */
|
|
26
|
+
frames: Promise<RecordingFrame[]>;
|
|
27
|
+
/** Resolves once the station has stopped sending this recording, which can be after `frames` rejected. */
|
|
28
|
+
drained: Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Request one recording and collect its frames until `CMD_DOWNLOAD_FINISH`. Only frames on
|
|
32
|
+
* {@link RECORDING_DATA_TYPE} tagged with the camera's channel belong to it, so a live stream open on the same
|
|
33
|
+
* station is never mixed in. The finish frame is taken on the camera's channel or channel 0.
|
|
34
|
+
*
|
|
35
|
+
* `frames` rejects with {@link RecordingDownloadError}: `no-data` when nothing arrives within
|
|
36
|
+
* {@link FIRST_FRAME_WAIT_MS}, and `incomplete` when the transfer ends without its finish frame, on a
|
|
37
|
+
* {@link IDLE_END_MS} silence, on `timeoutMs` or on {@link MAX_TRANSFER_BYTES}. An abort rejects it at once
|
|
38
|
+
* with the signal's reason. After a time bound, the byte ceiling or an abort, the transfer stops collecting
|
|
39
|
+
* but keeps listening until the station finishes or goes quiet, and only then resolves `drained`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function receiveRecording(session: P2PSession, request: {
|
|
42
|
+
path: string;
|
|
43
|
+
accountId: string;
|
|
44
|
+
channel: number;
|
|
45
|
+
timeoutMs?: number;
|
|
46
|
+
signal?: AbortSignal;
|
|
47
|
+
}): RecordingTransfer;
|
|
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
|
|
52
|
+
* {@link RecordingDownloadError} `undecodable` when no video frame decodes.
|
|
53
|
+
*/
|
|
54
|
+
export declare function decodeRecording(frames: readonly RecordingFrame[], eccPrivateKeyHex: string): RecordingDownload;
|
|
@@ -16,7 +16,7 @@ export interface SharedLiveSourceOptions {
|
|
|
16
16
|
makeStream: (ctx: {
|
|
17
17
|
reassertWanted: () => boolean;
|
|
18
18
|
}) => LiveStreamHandle;
|
|
19
|
-
/** No-consumer grace before teardown (default 8000ms). Distinct from the stream's keepalive. */
|
|
19
|
+
/** No-consumer grace before teardown (default 8000ms; 0 schedules teardown next tick). Distinct from the stream's keepalive. */
|
|
20
20
|
lingerMs?: number;
|
|
21
21
|
/** Per-consumer bounded queue depth; overflow → drop-to-keyframe (default 900 ≈ 30s @ 30fps). */
|
|
22
22
|
maxQueue?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mega-yfue/eufy-sdk",
|
|
3
|
-
"version": "0.4.0-beta.
|
|
3
|
+
"version": "0.4.0-beta.21",
|
|
4
4
|
"description": "One typed TypeScript client for the Anker eufy v6 cloud — capability-driven devices, realtime events over P2P/MQTT/push, and live media",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "mega-yfue",
|