@mega-yfue/eufy-sdk 0.2.0-beta.3 → 0.2.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.
- package/dist/client/device-registry.d.ts +14 -28
- package/dist/client/eufy-mega.d.ts +16 -11
- package/dist/client/types.d.ts +6 -2
- package/dist/core/contracts.d.ts +26 -30
- package/dist/core/crypto.d.ts +10 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/logger.d.ts +5 -3
- package/dist/core/solix-types.d.ts +121 -0
- package/dist/core/store.d.ts +38 -10
- package/dist/index.js +2929 -505
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/access.d.ts +22 -3
- package/dist/model/capabilities/arming.d.ts +33 -27
- package/dist/model/capabilities/battery.d.ts +32 -4
- package/dist/model/capabilities/contact.d.ts +4 -0
- package/dist/model/capabilities/display.d.ts +85 -0
- package/dist/model/capabilities/doorbell.d.ts +24 -14
- package/dist/model/capabilities/index.d.ts +19 -4
- package/dist/model/capabilities/lock.d.ts +15 -12
- package/dist/model/capabilities/ptz.d.ts +6 -2
- package/dist/model/capabilities/solix.d.ts +173 -0
- package/dist/model/capabilities/types.d.ts +69 -12
- package/dist/model/capabilities/vacuum-clean.d.ts +133 -25
- package/dist/model/classify.d.ts +3 -1
- package/dist/model/device-family.d.ts +2 -1
- package/dist/model/device-types.d.ts +1 -0
- package/dist/model/device.d.ts +15 -0
- package/dist/model/index.d.ts +5 -0
- package/dist/model/param-dictionary.d.ts +24 -0
- package/dist/model/param-namespace.d.ts +1 -1
- package/dist/model/solix-catalog.d.ts +25 -0
- package/dist/model/solix-device.d.ts +137 -0
- package/dist/model/solix-family.d.ts +31 -0
- package/dist/model/solix-site.d.ts +70 -0
- package/dist/model/types.d.ts +5 -5
- package/dist/transport/ff09.d.ts +7 -0
- package/dist/transport/http/decodeImageV2.d.ts +8 -14
- package/dist/transport/http/index.d.ts +1 -0
- package/dist/transport/http/jpeg-scan.d.ts +59 -0
- package/dist/transport/http/media-download.d.ts +3 -0
- package/dist/transport/http/mega-client.d.ts +89 -9
- package/dist/transport/http/solix-client.d.ts +270 -0
- package/dist/transport/http/solix-constants.d.ts +56 -0
- package/dist/transport/media-failure.d.ts +48 -0
- package/dist/transport/mqtt/index.d.ts +3 -0
- package/dist/transport/mqtt/secure-mqtt.d.ts +14 -1
- package/dist/transport/mqtt/solix-mqtt.d.ts +360 -0
- package/dist/transport/mqtt/topics.d.ts +30 -0
- package/dist/transport/p2p/command-router.d.ts +131 -19
- package/dist/transport/p2p/live-stream.d.ts +5 -4
- package/dist/transport/p2p/live-trace.d.ts +48 -5
- package/dist/transport/p2p/media.d.ts +11 -0
- package/dist/transport/p2p/p2p-session.d.ts +9 -0
- package/dist/transport/p2p/session-manager.d.ts +57 -23
- package/dist/transport/p2p/shared-live-source.d.ts +10 -1
- package/dist/transport/p2p/station-channels.d.ts +54 -0
- package/dist/transport/stored-image-cache.d.ts +7 -1
- package/package.json +7 -6
|
@@ -67,7 +67,7 @@ export interface LiveStreamOptions {
|
|
|
67
67
|
* How long an attached stream tolerates silence on its own channel before re-asserting again, in ms.
|
|
68
68
|
*
|
|
69
69
|
* The re-assert is settled by the first own-channel frame, because settling it is what stops two attached
|
|
70
|
-
* streams contending
|
|
70
|
+
* streams contending over one session, which serves one camera at a time. Silence for this long says the station
|
|
71
71
|
* is no longer serving this camera, which is the only condition the re-assert was for. Defaults to twice
|
|
72
72
|
* the keepalive interval, so a stream whose media flows never reaches it.
|
|
73
73
|
*/
|
|
@@ -129,8 +129,9 @@ export declare class LiveStream extends EventEmitter {
|
|
|
129
129
|
* Stop re-issuing the media start once this camera's own media has arrived, on an attached camera.
|
|
130
130
|
*
|
|
131
131
|
* The nudge differs by topology and only one branch is a ping: an own-session camera sends a small
|
|
132
|
-
* keepalive, while an attached camera has no such state and re-sends the FULL media start.
|
|
133
|
-
* serves one camera at a time that restart re-asserts this channel against whatever else is warm
|
|
132
|
+
* keepalive, while an attached camera has no such state and re-sends the FULL media start. Over one session,
|
|
133
|
+
* which serves one camera at a time, that restart re-asserts this channel against whatever else is warm on
|
|
134
|
+
* it, so two
|
|
134
135
|
* attached streams restart every interval and contend for the station continuously — measured on a real base
|
|
135
136
|
* as a full start every 3 s from each.
|
|
136
137
|
*
|
|
@@ -185,7 +186,7 @@ export declare class LiveStream extends EventEmitter {
|
|
|
185
186
|
*
|
|
186
187
|
* The match is UNCONDITIONAL, however long a station serves another camera instead of this one.
|
|
187
188
|
*
|
|
188
|
-
*
|
|
189
|
+
* One session serving one camera at a time hands a newly opened camera nothing but its sibling's frames until
|
|
189
190
|
* it switches, so "no media of my own yet, plenty for someone else" is what an ordinary handover looks like
|
|
190
191
|
* and does not distinguish a station that tags differently from one that is simply busy.
|
|
191
192
|
*
|
|
@@ -63,6 +63,20 @@ export type LiveTrace =
|
|
|
63
63
|
phase: "sequence-restart";
|
|
64
64
|
dataType: number;
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Which lookup channels a connection can ask for the station on, before it asks.
|
|
68
|
+
*
|
|
69
|
+
* A station is found by a local lookup, by a cloud lookup, or by both, and each needs something the other
|
|
70
|
+
* does not: the local one needs the station on this link, the cloud one needs both a key for the station and
|
|
71
|
+
* an address to ask. A connect that had one channel failed for that channel's reason alone, and a connect
|
|
72
|
+
* that had neither could not have succeeded — outcomes a station that is switched off is otherwise
|
|
73
|
+
* indistinguishable from, because nothing else in a failed connect states what was even attempted.
|
|
74
|
+
*/
|
|
75
|
+
| {
|
|
76
|
+
phase: "lookup-channels";
|
|
77
|
+
local: boolean;
|
|
78
|
+
cloud: boolean;
|
|
79
|
+
}
|
|
66
80
|
/**
|
|
67
81
|
* Work on a station is holding for its session to connect, with the milliseconds it will wait.
|
|
68
82
|
*
|
|
@@ -150,17 +164,30 @@ export type LiveTrace =
|
|
|
150
164
|
/**
|
|
151
165
|
* A station was resolved for a call, stating what the caller's device is on it and whose station it is.
|
|
152
166
|
*
|
|
153
|
-
* Emitted before
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
167
|
+
* Emitted before the session is waited on, so a station that is never reached still has this record: an
|
|
168
|
+
* attached camera's media start has no unencrypted form, so whether a device was taken as attached decides
|
|
169
|
+
* what its failure means. `stationAdmin` states whether the signed-in account is the station's
|
|
170
|
+
* administrator, which is what a key the account cannot resolve turns on; `unstated` is a device record
|
|
171
|
+
* that names no administrator, which is not the same as naming another. `stationModel` is the model of the
|
|
172
|
+
* station the call resolved — the base's for an attached camera, the device's own where it is its own
|
|
173
|
+
* station — absent where that record states none; without it a base this SDK reaches differently is
|
|
174
|
+
* indistinguishable from one that is switched off.
|
|
158
175
|
*/
|
|
159
176
|
| {
|
|
160
177
|
phase: "station-resolved";
|
|
161
178
|
topology: "attached" | "own";
|
|
162
179
|
channel: number;
|
|
163
180
|
stationAdmin: "self" | "other" | "unstated";
|
|
181
|
+
stationModel?: string;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The call's device has no usable channel on the station it resolved: its record states none (`missing`), or
|
|
185
|
+
* another device attached to the same station states the same one (`shared`). The call is refused with
|
|
186
|
+
* `DeviceChannelUnresolvedError` and nothing is sent.
|
|
187
|
+
*/
|
|
188
|
+
| {
|
|
189
|
+
phase: "station-channel-unresolved";
|
|
190
|
+
issue: "missing" | "shared";
|
|
164
191
|
}
|
|
165
192
|
/** A shared source began warming, with the interval it re-issues on and the deadline it fails at. */
|
|
166
193
|
| {
|
|
@@ -188,6 +215,22 @@ export type LiveTrace =
|
|
|
188
215
|
| {
|
|
189
216
|
phase: "path-stale";
|
|
190
217
|
silentMs: number;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* A stream received nothing on its own channel for the stall window, and what was done about it.
|
|
221
|
+
*
|
|
222
|
+
* A station that switches to a sibling leaves the stream it was serving with no frames, no error and no
|
|
223
|
+
* stop, so this silence is the only statement that it happened. `reasserted` re-issued the media start,
|
|
224
|
+
* which is the repair; `declined` left the channel alone because nothing is attached to this pull and
|
|
225
|
+
* taking the station back would take it from a camera someone is watching.
|
|
226
|
+
*
|
|
227
|
+
* Media still arriving means this never fires, so a picture that stopped advancing while this is silent
|
|
228
|
+
* stopped for a reason upstream of the station's attention.
|
|
229
|
+
*/
|
|
230
|
+
| {
|
|
231
|
+
phase: "channel-silent";
|
|
232
|
+
silentMs: number;
|
|
233
|
+
outcome: "reasserted" | "declined";
|
|
191
234
|
};
|
|
192
235
|
/**
|
|
193
236
|
* Record one startup observation at debug level.
|
|
@@ -34,11 +34,22 @@ export declare function openLiveStream(session: P2PSession, opts?: LiveStreamOpt
|
|
|
34
34
|
* The header states the stream's geometry when the capture started, and a stream that reconfigures
|
|
35
35
|
* mid-burst leaves it describing something the returned bytes contradict; the return value describes an
|
|
36
36
|
* image, so the image is its source of truth.
|
|
37
|
+
*
|
|
38
|
+
* The consumer is detached the moment the collected run is complete, and the decode that follows holds no
|
|
39
|
+
* station: it works on bytes already in memory. One session serves one camera at a time and a still never
|
|
40
|
+
* opens a second one, so a still that kept its pull attached across its own decode would deny
|
|
41
|
+
* that station to every live request for the length of an FFmpeg run — measured on a real base as a live
|
|
42
|
+
* request refused 370ms after the still it was waiting on had already collected everything it needed.
|
|
43
|
+
*
|
|
44
|
+
* `signal` ends the collection itself, not only the wait for it, and rejects with the signal's own reason
|
|
45
|
+
* because the abandonment is the caller's fact and not a failure of the source. It reaches only the
|
|
46
|
+
* collection: past that the station is already free, so there is nothing left for it to release.
|
|
37
47
|
*/
|
|
38
48
|
export declare function captureSnapshotFromShared(source: SharedLiveSource, opts?: {
|
|
39
49
|
timeoutMs?: number;
|
|
40
50
|
collectMs?: number;
|
|
41
51
|
skipKeyframes?: number;
|
|
52
|
+
signal?: AbortSignal;
|
|
42
53
|
logger?: Logger;
|
|
43
54
|
ffmpegLevel?: FfmpegLevel;
|
|
44
55
|
ffmpegPath?: string;
|
|
@@ -16,6 +16,15 @@
|
|
|
16
16
|
import { EventEmitter } from "node:events";
|
|
17
17
|
import { type Address, type P2PDataFrameHeader } from "./codec.js";
|
|
18
18
|
import { type Logger } from "../../core/logger.js";
|
|
19
|
+
/**
|
|
20
|
+
* How long a station is given to answer a lookup before the connection gives up on it and closes.
|
|
21
|
+
*
|
|
22
|
+
* The whole deadline for reaching a station: the lookups are re-sent every second until one is answered, and
|
|
23
|
+
* a connection that reaches this closes itself, so nothing addressed to that station can succeed afterwards.
|
|
24
|
+
* Published because it bounds every wait on a session connecting — a second number for the same deadline
|
|
25
|
+
* elsewhere would outlive the connection it waits on and charge the difference to every failure.
|
|
26
|
+
*/
|
|
27
|
+
export declare const CONNECT_TIMEOUT_MS = 15000;
|
|
19
28
|
/**
|
|
20
29
|
* The channel a command addresses the station itself on, rather than one of its cameras, and the value a
|
|
21
30
|
* session's channel-taking methods resolve an omitted channel to.
|
|
@@ -26,24 +26,35 @@ export interface SessionManagerOpts {
|
|
|
26
26
|
batteryIdleMs?: number;
|
|
27
27
|
/** Keepalive a single command holds after dispatch (ms). Default {@link COMMAND_KEEPALIVE_MS}. */
|
|
28
28
|
commandKeepAliveMs?: number;
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Power tier per station serial — injected by the facade (no model import). Default: everything
|
|
31
|
+
* `wired`. Asked about the STATION a session connects to, never the key it is filed under, so every
|
|
32
|
+
* session to one station gets that station's idle window.
|
|
33
|
+
*/
|
|
30
34
|
poweredFor?: (parentSn: string) => PowerTier;
|
|
31
35
|
/**
|
|
32
|
-
* Called
|
|
33
|
-
* deferred reset falling due.
|
|
36
|
+
* Called with the KEY of a session the manager closed on its OWN initiative — an elapsed idle window,
|
|
37
|
+
* or a deferred reset falling due. The key, not the station: several sessions can share a station and
|
|
38
|
+
* only the one that closed is stale, so an owner told the station would tear down connections that are
|
|
39
|
+
* still serving.
|
|
34
40
|
*
|
|
35
41
|
* Those two are the only closes with no caller to follow up: everything riding the session is stale the
|
|
36
42
|
* moment it goes, and only the owner knows what that is. A close a caller asked for is that caller's to
|
|
37
43
|
* clean up after, which is why this does not fire for {@link SessionManager.close},
|
|
38
44
|
* {@link SessionManager.closeAll}, or a superseded open.
|
|
39
45
|
*/
|
|
40
|
-
onAutoClose?: (
|
|
46
|
+
onAutoClose?: (key: string) => void;
|
|
41
47
|
/** Diagnostics sink for the lifecycle transitions (open / idle-arm / detach). Omit for silence. */
|
|
42
48
|
logger?: Logger;
|
|
43
49
|
}
|
|
44
50
|
/**
|
|
45
|
-
* Manages P2P sessions
|
|
46
|
-
* `P2PSession` (it owns the socket
|
|
51
|
+
* Manages P2P sessions by **key**, each recording the station it connects to. The router builds and
|
|
52
|
+
* wires the actual `P2PSession` (it owns the socket and event fan-out); this decides open/close timing.
|
|
53
|
+
*
|
|
54
|
+
* A key is the station's serial for the one session that carries its control traffic and its events.
|
|
55
|
+
* Where a station has to serve more than one camera at once it also holds a session per camera, filed
|
|
56
|
+
* under a key of the router's choosing and recording the same station — so each has its own refcount and
|
|
57
|
+
* its own idle window, and releasing one never disturbs another.
|
|
47
58
|
*/
|
|
48
59
|
export declare class SessionManager {
|
|
49
60
|
private readonly opts;
|
|
@@ -53,23 +64,40 @@ export declare class SessionManager {
|
|
|
53
64
|
private readonly logger;
|
|
54
65
|
constructor(opts?: SessionManagerOpts);
|
|
55
66
|
/** The live session for a station, or `undefined` if not open. */
|
|
56
|
-
get(
|
|
57
|
-
/**
|
|
67
|
+
get(key: string): P2PSession | undefined;
|
|
68
|
+
/** Keys of the open sessions. */
|
|
58
69
|
keys(): string[];
|
|
59
|
-
/** A plain `Map<
|
|
70
|
+
/** A plain `Map<key, P2PSession>` snapshot of the open sessions (for `getSessions()` / tests). */
|
|
60
71
|
liveSessions(): Map<string, P2PSession>;
|
|
61
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Get or create the lifecycle entry under `key`, recording which station it connects to. An existing
|
|
74
|
+
* entry keeps the station it was opened with — the key owns one connection for its lifetime, and a
|
|
75
|
+
* later caller passing a different station would otherwise re-point a live entry's power tier.
|
|
76
|
+
*/
|
|
62
77
|
private entry;
|
|
63
|
-
/** Register an already-built session for test seeding or an externally assembled connection. */
|
|
64
|
-
register(parentSn: string, session: P2PSession): void;
|
|
65
78
|
/**
|
|
66
|
-
*
|
|
79
|
+
* Register an already-built session for test seeding or an externally assembled connection.
|
|
80
|
+
*
|
|
81
|
+
* `station` defaults to the key, which is safe HERE and nowhere else in this class: the paths that open a
|
|
82
|
+
* session under a key that is not a station serial all go through {@link acquire}, which requires it. A
|
|
83
|
+
* caller seeding one under such a key must pass it.
|
|
84
|
+
*/
|
|
85
|
+
register(key: string, session: P2PSession, station?: string): void;
|
|
86
|
+
/**
|
|
87
|
+
* Ensure a session to `key` is open, building it via `factory` if cold. Concurrent calls for the
|
|
67
88
|
* same cold station share ONE connect (the `connecting` promise); `factory` builds + wires + awaits
|
|
68
89
|
* `connect()` and resolves the connected session.
|
|
69
90
|
*/
|
|
70
|
-
acquire(
|
|
71
|
-
/**
|
|
72
|
-
|
|
91
|
+
acquire(key: string, factory: (register: (session: P2PSession) => void) => Promise<P2PSession>, station: string): Promise<P2PSession>;
|
|
92
|
+
/**
|
|
93
|
+
* Add a reason to stay connected; cancels a pending idle-close.
|
|
94
|
+
*
|
|
95
|
+
* Refused when nothing is open under `key`, for the reason {@link release} gives in the other direction: a
|
|
96
|
+
* retain names a session that was acquired, and one that names nothing would file an entry with no
|
|
97
|
+
* connection behind it. {@link hold} is the path that legitimately creates one, and it opens the entry
|
|
98
|
+
* itself before retaining it.
|
|
99
|
+
*/
|
|
100
|
+
retain(key: string): void;
|
|
73
101
|
/**
|
|
74
102
|
* Release a reason; arm the idle-close when the last one goes.
|
|
75
103
|
*
|
|
@@ -78,22 +106,28 @@ export declare class SessionManager {
|
|
|
78
106
|
* from scratch or complete a deferred reset a real viewer has not yet earned. Clamping to zero did
|
|
79
107
|
* both silently.
|
|
80
108
|
*/
|
|
81
|
-
release(
|
|
109
|
+
release(key: string): void;
|
|
82
110
|
/**
|
|
83
111
|
* Hold a session warm for `commandKeepAliveMs` after a control command, then release. A burst of
|
|
84
112
|
* commands each re-holds before the previous release fires, so the session never idles mid-burst.
|
|
85
113
|
*/
|
|
86
|
-
bumpCommand(
|
|
114
|
+
bumpCommand(key: string, station: string): void;
|
|
87
115
|
/**
|
|
88
|
-
* Retain a
|
|
116
|
+
* Retain a session and release it again after `ms` — the primitive behind command-keepalive and event
|
|
89
117
|
* pre-warm, and the only way to hold one open without an attachment to release it.
|
|
90
118
|
*
|
|
119
|
+
* `station` is required rather than defaulted from the key, because this is the one path that can
|
|
120
|
+
* CREATE an entry: a pre-warm takes its hold before the open. An entry filed under a media key with
|
|
121
|
+
* that key as its own station would be asked for the power tier of a serial that does not exist, be
|
|
122
|
+
* answered `wired`, and never idle-detach — which on a battery station is the drain this class exists
|
|
123
|
+
* to prevent, and is invisible until the battery is flat.
|
|
124
|
+
*
|
|
91
125
|
* The timer is owned by the entry, so {@link discard} cancels it. That ownership is the point: keyed
|
|
92
126
|
* only by serial, an expiring hold would otherwise outlive the entry it was taken on and release a
|
|
93
127
|
* retain counted by the SUCCESSOR entry — dropping a live viewer's count and arming an idle-detach
|
|
94
128
|
* underneath it.
|
|
95
129
|
*/
|
|
96
|
-
hold(
|
|
130
|
+
hold(key: string, ms: number, station: string): void;
|
|
97
131
|
/**
|
|
98
132
|
* Arm the idle-close timer for a station whose retain count just reached zero. A wired station with
|
|
99
133
|
* an infinite window is left persistent (no timer). Any subsequent {@link retain} cancels it.
|
|
@@ -123,9 +157,9 @@ export declare class SessionManager {
|
|
|
123
157
|
*/
|
|
124
158
|
private autoClose;
|
|
125
159
|
/** Drop a station's entry + timer (called from the session's `close` handler). Idempotent. */
|
|
126
|
-
remove(
|
|
160
|
+
remove(key: string): void;
|
|
127
161
|
/** Close one station now and discard its lifecycle entry. */
|
|
128
|
-
close(
|
|
162
|
+
close(key: string): Promise<void>;
|
|
129
163
|
/**
|
|
130
164
|
* Reset once every viewer detaches, ignoring only expiring holds.
|
|
131
165
|
*
|
|
@@ -136,7 +170,7 @@ export declare class SessionManager {
|
|
|
136
170
|
* Both branches close through {@link autoClose}: the caller asked for a recycle, not for the station's
|
|
137
171
|
* live sources to be dropped, so it does not clean up after one — exactly like the idle path.
|
|
138
172
|
*/
|
|
139
|
-
resetWhenUnused(
|
|
173
|
+
resetWhenUnused(key: string): Promise<void>;
|
|
140
174
|
/** Settle a discarded entry's reset callers with the same outcome as its session close. */
|
|
141
175
|
private settleReset;
|
|
142
176
|
/** Close one discarded entry and settle only its own reset callers before preserving any failure. */
|
|
@@ -10,7 +10,7 @@ export interface SharedLiveSourceOptions {
|
|
|
10
10
|
* `.start()` itself.
|
|
11
11
|
*
|
|
12
12
|
* `ctx.reassertWanted` answers whether this pull still has anyone attached. A stream that re-asserts a
|
|
13
|
-
* channel to hold it open should consult it, so a pull nothing is watching stops competing for a
|
|
13
|
+
* channel to hold it open should consult it, so a pull nothing is watching stops competing for a session
|
|
14
14
|
* that serves one camera at a time.
|
|
15
15
|
*/
|
|
16
16
|
makeStream: (ctx: {
|
|
@@ -73,6 +73,15 @@ export interface SharedLiveSourceOptions {
|
|
|
73
73
|
* an owner does in response to this callback.
|
|
74
74
|
*/
|
|
75
75
|
onStartFailed?: () => void;
|
|
76
|
+
/**
|
|
77
|
+
* The pull has ended and will not resume: the linger elapsed, the battery budget ran out, or the source
|
|
78
|
+
* was torn down. A later attach builds a fresh stream rather than reviving this one.
|
|
79
|
+
*
|
|
80
|
+
* Distinct from {@link onIdle} by what is still running. `onIdle` fires at the last detach, while the
|
|
81
|
+
* linger is still holding the pull open so a quick re-attach costs nothing; between the two the pull is
|
|
82
|
+
* alive. This fires when it is not.
|
|
83
|
+
*/
|
|
84
|
+
onStopped?: () => void;
|
|
76
85
|
/**
|
|
77
86
|
* A media start was abandoned unacknowledged before anything was delivered, so this session is not being
|
|
78
87
|
* heard. The owner is asked for a replacement and calls {@link SharedLiveSource.rewarm} once it has one.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Station topology and per-station channel addressing, from the device records alone.
|
|
3
|
+
*
|
|
4
|
+
* Both sides of the P2P wire key on a device's channel within its station: a media start and every
|
|
5
|
+
* per-channel command address it, and an inbound frame is attributed to the device on its channel. Resolving
|
|
6
|
+
* it once, here, keeps the two sides from ever disagreeing about which device a channel means.
|
|
7
|
+
*
|
|
8
|
+
* @module transport/p2p/station-channels
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
import type { EufyDevice } from "../../core/types.js";
|
|
12
|
+
/**
|
|
13
|
+
* The station a device's traffic belongs to, from its cloud record and its own serial.
|
|
14
|
+
*
|
|
15
|
+
* `parent_sn` carries the parent on a HomeBase-attached device. `station_sn` is frequently absent there —
|
|
16
|
+
* empty on every attached sensor of a T8010 — and serves only as a fallback. An empty string states no
|
|
17
|
+
* station.
|
|
18
|
+
*
|
|
19
|
+
* A device naming no parent answers its own serial, so every device has a station.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolvedStationSn(raw: Record<string, unknown>, sn: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* The parent station a device's traffic belongs to — the session key the router opens and the station the
|
|
24
|
+
* frame resolver attributes by.
|
|
25
|
+
*
|
|
26
|
+
* `parent_sn` on the cloud record is the field that is actually populated for a HomeBase-attached device, so
|
|
27
|
+
* it wins; `stationSn` is frequently absent (observed empty on every attached sensor of a T8010), so keying on
|
|
28
|
+
* it alone silently resolves an attached device to ITSELF and no frame ever matches. Answering the device's
|
|
29
|
+
* OWN serial is what "stands alone" means.
|
|
30
|
+
*/
|
|
31
|
+
export declare function stationOf(dev: EufyDevice): string;
|
|
32
|
+
/**
|
|
33
|
+
* A device's channel within its station, or why it has none. `shared` keeps the channel it `claimed`, so a
|
|
34
|
+
* frame arriving on it is known to belong to one of the claimants rather than to the station.
|
|
35
|
+
*/
|
|
36
|
+
export type StationChannel = {
|
|
37
|
+
channel: number;
|
|
38
|
+
} | {
|
|
39
|
+
issue: "missing";
|
|
40
|
+
} | {
|
|
41
|
+
issue: "shared";
|
|
42
|
+
claimed: number;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* The channel each device is addressed by within its station.
|
|
46
|
+
*
|
|
47
|
+
* A standalone device is its own station, addressed by the channel its record states, or 0. An attached
|
|
48
|
+
* device has a channel only when its record states one that no other device attached to the same station
|
|
49
|
+
* also states. A missing channel names no device, and a channel two attached devices both state cannot say
|
|
50
|
+
* which of them it means, so either resolves to an issue rather than to a channel: guessing would address
|
|
51
|
+
* another device, streaming its video or attributing its frames under this serial. Every attached device
|
|
52
|
+
* counts towards a clash, whatever its kind, and the answer does not depend on list order.
|
|
53
|
+
*/
|
|
54
|
+
export declare function stationChannels(devices: readonly EufyDevice[]): Map<string, StationChannel>;
|
|
@@ -10,7 +10,13 @@ export declare class StoredImageCache {
|
|
|
10
10
|
private activeDownloads;
|
|
11
11
|
private generation;
|
|
12
12
|
constructor(downloader: (url: string, deviceKey: string) => Promise<Buffer>, logger: Logger, clock?: () => number, isLifecycleError?: (error: unknown) => boolean);
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Observe a normalized thumbnail URL and start acquisition eagerly.
|
|
15
|
+
*
|
|
16
|
+
* A URL already inside this device's window of recent attempts is ignored, so one event arriving as
|
|
17
|
+
* several pushes downloads one thumbnail. The window is a `Set`, which iterates in insertion order,
|
|
18
|
+
* so the entry evicted once it is full is the oldest attempt.
|
|
19
|
+
*/
|
|
14
20
|
observe(deviceKey: string, url: string): void;
|
|
15
21
|
/** Return retained bytes without starting or awaiting network work. */
|
|
16
22
|
snapshotStored(deviceKey: string): Promise<Buffer>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mega-yfue/eufy-sdk",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.30",
|
|
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",
|
|
@@ -64,7 +64,8 @@
|
|
|
64
64
|
"guard:docrefs": "bash scripts/ci/guard-doc-refs.sh",
|
|
65
65
|
"guard:lines": "bash scripts/ci/guard-lines.sh",
|
|
66
66
|
"check:esm": "node -e \"import('./dist/index.js').then(m=>console.log('ESM OK:',Object.keys(m).length,'exports'))\"",
|
|
67
|
-
"
|
|
67
|
+
"check:snippets": "node scripts/ci/check-doc-snippets.mjs",
|
|
68
|
+
"verify": "npm run format:check && npm run typecheck && npm run guard:decorrelation && npm run guard:lines && npm run guard:docrefs && npm run guard:consumer-agnostic && npm run guard:capability-ownership && npm run build && npm run check:esm && npm run check:snippets && npm run typecheck:examples && npm test",
|
|
68
69
|
"release": "bash scripts/release.sh"
|
|
69
70
|
},
|
|
70
71
|
"engines": {
|
|
@@ -72,14 +73,14 @@
|
|
|
72
73
|
},
|
|
73
74
|
"dependencies": {
|
|
74
75
|
"jpeg-js": "^0.4.4",
|
|
75
|
-
"mqtt": "^5.
|
|
76
|
+
"mqtt": "^5.16.0",
|
|
76
77
|
"protobufjs": "^8.8.0"
|
|
77
78
|
},
|
|
78
79
|
"devDependencies": {
|
|
79
|
-
"@types/node": "^26.
|
|
80
|
+
"@types/node": "^26.6.2",
|
|
80
81
|
"esbuild": "^0.28.2",
|
|
81
|
-
"prettier": "^3.9.
|
|
82
|
+
"prettier": "^3.9.9",
|
|
82
83
|
"typescript": "^7.0.2",
|
|
83
|
-
"vitest": "^5.0.
|
|
84
|
+
"vitest": "^5.0.1"
|
|
84
85
|
}
|
|
85
86
|
}
|