@mega-yfue/eufy-sdk 0.2.0-beta.1 → 0.2.0-beta.10
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 +5 -5
- package/dist/core/contracts.d.ts +58 -3
- 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 +36 -0
- package/dist/core/store.d.ts +20 -9
- package/dist/index.js +1142 -71
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/arming.d.ts +58 -28
- package/dist/model/capabilities/display.d.ts +85 -0
- package/dist/model/capabilities/index.d.ts +11 -5
- package/dist/model/capabilities/solix.d.ts +75 -0
- package/dist/model/capabilities/types.d.ts +16 -4
- package/dist/model/capabilities/vacuum-clean.d.ts +74 -25
- package/dist/model/index.d.ts +3 -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 +20 -0
- package/dist/model/solix-device.d.ts +102 -0
- package/dist/model/types.d.ts +5 -5
- package/dist/transport/ff09.d.ts +7 -0
- package/dist/transport/http/index.d.ts +1 -0
- package/dist/transport/http/solix-client.d.ts +158 -0
- package/dist/transport/http/solix-constants.d.ts +29 -0
- package/dist/transport/mqtt/index.d.ts +2 -0
- package/dist/transport/mqtt/secure-mqtt.d.ts +14 -1
- package/dist/transport/mqtt/solix-mqtt.d.ts +214 -0
- package/dist/transport/mqtt/topics.d.ts +20 -0
- package/dist/transport/p2p/command-router.d.ts +23 -0
- package/dist/transport/p2p/index.d.ts +1 -0
- package/dist/transport/p2p/live-trace.d.ts +100 -6
- package/dist/transport/p2p/media.d.ts +11 -0
- package/dist/transport/p2p/p2p-session.d.ts +4 -0
- package/package.json +3 -2
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { type SolixCapability, type SolixEnergyMeterReads } from "./capabilities/solix.js";
|
|
2
|
+
import type { SolixDeviceRecord, SolixProductCategory } from "../core/solix-types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The device record shape, re-exported from the model surface. It lives in `core/solix-types` so the
|
|
5
|
+
* transport client can return it without crossing the transport↔model line.
|
|
6
|
+
*/
|
|
7
|
+
export type { SolixDeviceRecord } from "../core/solix-types.js";
|
|
8
|
+
export interface SolixIdentity {
|
|
9
|
+
serial: string;
|
|
10
|
+
productCode: string;
|
|
11
|
+
/** Friendly name — the catalog marketing name if resolvable, else the record's alias/name. */
|
|
12
|
+
name: string;
|
|
13
|
+
/** Anker catalog category (e.g. "Accessory", "Portable Power Station"), if resolvable. */
|
|
14
|
+
category?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface SolixConnectivity {
|
|
17
|
+
online: boolean;
|
|
18
|
+
rssi?: number;
|
|
19
|
+
ssid?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The `dev.energyMeter()` handle: the members-derived reads ({@link SolixEnergyMeterReads} —
|
|
23
|
+
* `meterVoltageL1` is present only once a frame carrying its tag has landed). Every not-yet-named meter
|
|
24
|
+
* quantity is read from {@link SolixDevice.telemetry} under its `channel_<hex tag>` key instead, which a
|
|
25
|
+
* static members table cannot enumerate.
|
|
26
|
+
*/
|
|
27
|
+
export type SolixEnergyMeter = SolixEnergyMeterReads;
|
|
28
|
+
/** Options for {@link SolixDevice}. */
|
|
29
|
+
export interface SolixDeviceOptions {
|
|
30
|
+
/** Catalog categories (from `SolixClient.getProductCatalog()`) — used to resolve name + category. */
|
|
31
|
+
catalog?: SolixProductCategory[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A discovered Solix device with resolved category + capabilities. Feed live telemetry with
|
|
35
|
+
* {@link applyReading} (from {@link SolixMqtt}'s `reading` events) to populate value accessors.
|
|
36
|
+
*/
|
|
37
|
+
export declare class SolixDevice {
|
|
38
|
+
readonly serial: string;
|
|
39
|
+
readonly productCode: string;
|
|
40
|
+
readonly record: SolixDeviceRecord;
|
|
41
|
+
private readonly caps;
|
|
42
|
+
private readonly identity_;
|
|
43
|
+
private values;
|
|
44
|
+
constructor(record: SolixDeviceRecord, opts?: SolixDeviceOptions);
|
|
45
|
+
/** All capabilities this device carries. */
|
|
46
|
+
get capabilities(): SolixCapability[];
|
|
47
|
+
/** Whether the device carries a capability — the only correct way to branch on behaviour. */
|
|
48
|
+
has(capability: SolixCapability): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Merge a live telemetry reading (a `SolixMqtt` `reading` event) so accessors reflect it. Takes the
|
|
51
|
+
* WHOLE reading, not just its values, and drops one addressed to a different device: the documented
|
|
52
|
+
* wiring is `mqtt.on("reading", r => device.applyReading(r))`, and one MQTT stream carries every
|
|
53
|
+
* watched meter on the account — so without this filter two meters would cross-feed each other's floats.
|
|
54
|
+
* A reading with no `deviceSn` (a hand-built one) is accepted as-is.
|
|
55
|
+
*/
|
|
56
|
+
applyReading(reading: {
|
|
57
|
+
deviceSn?: string;
|
|
58
|
+
values: Record<string, number>;
|
|
59
|
+
}): void;
|
|
60
|
+
/** All decoded float telemetry channels from the latest applied reading (raw, `channel_<tag>` keys). */
|
|
61
|
+
telemetry(): Record<string, number>;
|
|
62
|
+
identity(): SolixIdentity;
|
|
63
|
+
firmware(): {
|
|
64
|
+
version: string;
|
|
65
|
+
} | undefined;
|
|
66
|
+
connectivity(): SolixConnectivity | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* The members-derived `energyMeter` reads, or `undefined` when the device has no meter. Each getter is
|
|
69
|
+
* installed only for a tag this device has actually reported, and reads `this.values` LIVE — a handle
|
|
70
|
+
* held across an {@link applyReading} reflects the newer values. Getter INSTALLATION is fixed at the
|
|
71
|
+
* time of this call, so re-call it to pick up a tag first seen since.
|
|
72
|
+
*/
|
|
73
|
+
energyMeter(): SolixEnergyMeter | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* The {@link MemberDeps} the members engine needs: a read closure over the live values store, and the
|
|
76
|
+
* evidence gate (`ctx.paramIds`) rebuilt from the ff09 tags this device has reported. No `codec` — the
|
|
77
|
+
* codecs are eufy transport families and a Solix device belongs to none of them. The sink is a no-op:
|
|
78
|
+
* Solix telemetry is read-only, no member here dispatches a command.
|
|
79
|
+
*/
|
|
80
|
+
private meterDeps;
|
|
81
|
+
/** The ff09 tags this device has reported, derived from the decoder's `channel_<hex>` keys. */
|
|
82
|
+
private seenTags;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The minimum a client must offer to be discovered against — the two wire reads {@link discoverSolixDevices}
|
|
86
|
+
* composes. Typed STRUCTURALLY (not as `SolixClient`) so the model layer never imports the transport
|
|
87
|
+
* client: the hard `transport ⊥ model` rule forbids it, and a structural shape needs no import.
|
|
88
|
+
*/
|
|
89
|
+
export interface SolixDeviceReader {
|
|
90
|
+
getDevices(): Promise<SolixDeviceRecord[]>;
|
|
91
|
+
getProductCatalog(): Promise<SolixProductCategory[]>;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Discover an account's Solix devices as capability-driven {@link SolixDevice} objects — the wire+model
|
|
95
|
+
* composition (a transport read + the product catalog) that used to be `SolixClient.discoverDevices()`.
|
|
96
|
+
* It lives in the model layer because it builds `SolixDevice`; the wire client (now `transport/http`)
|
|
97
|
+
* cannot, and passing it structurally keeps the layers decorrelated. Feed live telemetry to each result
|
|
98
|
+
* via `SolixDevice.applyReading`.
|
|
99
|
+
*/
|
|
100
|
+
export declare function discoverSolixDevices(client: SolixDeviceReader, opts?: {
|
|
101
|
+
catalog?: SolixProductCategory[];
|
|
102
|
+
}): Promise<SolixDevice[]>;
|
package/dist/model/types.d.ts
CHANGED
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
*
|
|
29
29
|
* `display` is the T87Ax Smart Display line — its own codec because its `device_type` collides with
|
|
30
30
|
* the security residual range (confirmed live, 2026-09-04: it connects over secure MQTT with no
|
|
31
|
-
* `p2p_did`, never P2P).
|
|
32
|
-
*
|
|
33
|
-
* targets it
|
|
34
|
-
*
|
|
31
|
+
* `p2p_did`, never P2P). It owns its own param namespace (ids 8001-8006) and its own product line, so
|
|
32
|
+
* no other line's capability can attach to it — see `namespaceForCodec` and `CODEC_LINE`. The `display`
|
|
33
|
+
* capability targets it and reads the charge; no screen/audio/assistant param has been observed, so
|
|
34
|
+
* those are absent rather than deferred.
|
|
35
35
|
*/
|
|
36
36
|
export type Codec = "station" | "camera" | "sensor" | "lock" | "keypad" | "vacuum" | "mower" | "light" | "printer" | "display";
|
|
37
37
|
/**
|
|
@@ -39,7 +39,7 @@ export type Codec = "station" | "camera" | "sensor" | "lock" | "keypad" | "vacuu
|
|
|
39
39
|
* it maps to a {@link CapabilityModule} that owns its property schema. Extend this union as
|
|
40
40
|
* new capabilities are modelled — adding one never requires a subclass.
|
|
41
41
|
*/
|
|
42
|
-
export type Capability = "video" | "snapshot" | "motion" | "person_detection" | "battery" | "light" | "ptz" | "doorbell" | "contact" | "leak" | "smoke" | "co" | "siren" | "lock" | "keypad" | "arming" | "storage" | "rtsp" | "camera" | "audio" | "vacuum_clean" | "vacuum_dock" | "suction" | "locate" | "smart_light" | "info";
|
|
42
|
+
export type Capability = "video" | "snapshot" | "motion" | "person_detection" | "battery" | "light" | "ptz" | "doorbell" | "contact" | "leak" | "smoke" | "co" | "siren" | "lock" | "keypad" | "arming" | "storage" | "rtsp" | "camera" | "audio" | "vacuum_clean" | "vacuum_dock" | "suction" | "locate" | "smart_light" | "display" | "info";
|
|
43
43
|
/** Value type of a property. */
|
|
44
44
|
export type PropertyValueType = "bool" | "number" | "string" | "enum";
|
|
45
45
|
/**
|
package/dist/transport/ff09.d.ts
CHANGED
|
@@ -397,6 +397,13 @@ export interface Ff09SettingsResponse {
|
|
|
397
397
|
* padding past the last real field.
|
|
398
398
|
*/
|
|
399
399
|
export declare function parseFf09SettingsResponse(plain: Buffer): Ff09SettingsResponse;
|
|
400
|
+
/**
|
|
401
|
+
* Walk a bounded `tag|len|value` TLV region into a `tag → bytes` map. Stops at a `0x00` tag (only ever
|
|
402
|
+
* trailing zero-padding, never a real field — real tags start at `0xa1`) and refuses a field whose
|
|
403
|
+
* declared length would overrun `end`, so a corrupt length can't read past the region (e.g. into a
|
|
404
|
+
* trailing checksum). Shared by {@link parseFf09SettingsResponse} and the Solix param decoder.
|
|
405
|
+
*/
|
|
406
|
+
export declare function walkFf09Tlv(buf: Buffer, start: number, end: number): Map<number, Buffer>;
|
|
400
407
|
/** Read a little-endian u16 out of a TLV field buffer (throws on a missing/short field — a caller-side bug, not a wire ambiguity). */
|
|
401
408
|
export declare function readFf09U16LE(field: Buffer | undefined, name: string): number;
|
|
402
409
|
/** Read a single byte out of a TLV field buffer (throws on a missing field — same convention as {@link readFf09U16LE}). */
|
|
@@ -3,3 +3,4 @@ export { randomPhoneModel, randomUserAgent } from "./phone-model.js";
|
|
|
3
3
|
export * from "./decodeImageV1.js";
|
|
4
4
|
export * from "./decodeImageV2.js";
|
|
5
5
|
export { listLightEffects, listAiSceneRecommendations, type LightEffectSummary } from "./light-catalog.js";
|
|
6
|
+
export { SolixClient, type SolixClientOptions, type SolixLoginResult, type SolixSession, type SolixPersisted, type SolixSessionStore, } from "./solix-client.js";
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal client for the Anker Solix power-station cloud, driven by the SAME account login the
|
|
3
|
+
* eufy client uses.
|
|
4
|
+
*
|
|
5
|
+
* Why this is separate from the eufy device client: Solix shares Anker's `algo_ecdh` passport (so
|
|
6
|
+
* {@link prepareKeyExchange} / {@link encryptLoginPassword} / {@link signRequest} are reused verbatim
|
|
7
|
+
* for the login handshake) but exposes a different device backend — its own `app-name`, host, and
|
|
8
|
+
* bootstrap key (`SOLIX_APP_NAME`, `SOLIX_DEFAULT_API_HOST`, {@link SOLIX_LOCAL_KEY_HEX}) —
|
|
9
|
+
* and its authenticated resource reads are PLAIN JSON, carrying only the auth token and a
|
|
10
|
+
* `gtoken = md5(user_id)`, with no per-request encryption or signature. This client therefore does
|
|
11
|
+
* the encrypted passport handshake to obtain a token, then makes plain authenticated reads.
|
|
12
|
+
*
|
|
13
|
+
* This is the wire client (transport layer): it returns the vendor's typed JSON as received. Building
|
|
14
|
+
* those records into capability-driven `SolixDevice` models is the model layer's job — see
|
|
15
|
+
* `discoverSolixDevices()` — so the two stay decorrelated (transport never imports model).
|
|
16
|
+
*/
|
|
17
|
+
import { type SessionStore, type SolixDeviceRecord, type SolixProductCategory } from "../../core/index.js";
|
|
18
|
+
import type { SecureMqttCredentials } from "../mqtt/secure-mqtt.js";
|
|
19
|
+
/** An authenticated Solix session — the token + the derived `gtoken` + the resolved API host. */
|
|
20
|
+
export interface SolixSession {
|
|
21
|
+
authToken: string;
|
|
22
|
+
userId: string;
|
|
23
|
+
/** `md5(user_id)` — sent as the `gtoken` header on every authenticated read. */
|
|
24
|
+
gtoken: string;
|
|
25
|
+
/** The regional API host the account resolved to (e.g. the EU shard). */
|
|
26
|
+
apiHost: string;
|
|
27
|
+
/** Unix seconds; 0 when the server did not supply one. */
|
|
28
|
+
tokenExpiresAt: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Outcome of {@link SolixClient.login}. `2fa` mirrors the eufy passport: the server sent a code and
|
|
32
|
+
* the client holds a limited token — call {@link SolixClient.submitVerifyCode} to finish.
|
|
33
|
+
*/
|
|
34
|
+
export type SolixLoginResult = {
|
|
35
|
+
status: "ok";
|
|
36
|
+
session: SolixSession;
|
|
37
|
+
} | {
|
|
38
|
+
status: "2fa";
|
|
39
|
+
method: string;
|
|
40
|
+
};
|
|
41
|
+
/** Options for {@link SolixClient}. */
|
|
42
|
+
export interface SolixClientOptions {
|
|
43
|
+
email: string;
|
|
44
|
+
password: string;
|
|
45
|
+
/** ISO-3166 alpha-2; defaults to "US". Sent as `country` and `ab`. */
|
|
46
|
+
countryCode?: string;
|
|
47
|
+
/** Override the API host (skips domain-estimate). Defaults to estimate → `SOLIX_DEFAULT_API_HOST`. */
|
|
48
|
+
apiHost?: string;
|
|
49
|
+
/** App version reported to the cloud. */
|
|
50
|
+
appVersion?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Stable per-install device id (UUID). The auth token is bound to it, and a shifting id looks like
|
|
53
|
+
* a new device each run and re-triggers 2FA. Defaults to a deterministic id derived from the email
|
|
54
|
+
* (stable across runs); a store's saved id wins over this.
|
|
55
|
+
*/
|
|
56
|
+
openudid?: string;
|
|
57
|
+
/** Persist the token + device id so a dedicated account logs in once and reuses it until expiry. */
|
|
58
|
+
store?: SolixSessionStore;
|
|
59
|
+
/** Injected fetch (for tests). Defaults to the global `fetch`. */
|
|
60
|
+
fetchImpl?: typeof fetch;
|
|
61
|
+
}
|
|
62
|
+
/** What {@link SolixSessionStore} holds: the stable device id and (once logged in) the session. */
|
|
63
|
+
export interface SolixPersisted {
|
|
64
|
+
openudid: string;
|
|
65
|
+
session?: SolixSession;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A place to persist a Solix session across process runs — the core {@link SessionStore} parameterised on
|
|
69
|
+
* the Solix record shape, so `FileSessionStore` serves it as-is. The device id survives token expiry (so
|
|
70
|
+
* the account keeps seeing the same device and does not re-prompt 2FA), and a live session is reused
|
|
71
|
+
* until it expires.
|
|
72
|
+
*/
|
|
73
|
+
export type SolixSessionStore = SessionStore<SolixPersisted>;
|
|
74
|
+
/**
|
|
75
|
+
* Login + read client for one Anker account's Solix devices. Construct with the account
|
|
76
|
+
* credentials, `await login()`, then read {@link getDevices} / {@link getSites} / {@link
|
|
77
|
+
* getUserMqttInfo}. Not tied to any host runtime.
|
|
78
|
+
*/
|
|
79
|
+
export declare class SolixClient {
|
|
80
|
+
private readonly email;
|
|
81
|
+
private readonly password;
|
|
82
|
+
private readonly country;
|
|
83
|
+
private readonly appVersion;
|
|
84
|
+
private readonly doFetch;
|
|
85
|
+
private readonly store?;
|
|
86
|
+
private readonly openudid;
|
|
87
|
+
private apiHost;
|
|
88
|
+
private session_?;
|
|
89
|
+
/** Carried between {@link login} and {@link submitVerifyCode} while a 2FA code is outstanding. */
|
|
90
|
+
private pending2fa?;
|
|
91
|
+
/**
|
|
92
|
+
* Resolve the device id (explicit → stored → deterministic from the email, so it is stable and does
|
|
93
|
+
* not re-trigger 2FA) and adopt a stored session that has not expired, so a warm start skips the
|
|
94
|
+
* handshake. An explicit `opts.apiHost` outranks a stored session's host in both cases: it is an
|
|
95
|
+
* override that also skips domain-estimate, and every read goes through `this.apiHost`.
|
|
96
|
+
*/
|
|
97
|
+
constructor(opts: SolixClientOptions);
|
|
98
|
+
/** Persist the current device id (+ session, if any) when a store is configured. */
|
|
99
|
+
private persist;
|
|
100
|
+
/** The authenticated session, once {@link login} has resolved to `ok`. */
|
|
101
|
+
get session(): SolixSession | undefined;
|
|
102
|
+
/**
|
|
103
|
+
* Headers for the login/key-exchange path, which carry the device id. Authenticated resource reads
|
|
104
|
+
* must NOT send `openudid` — the gateway rejects a token-bearing read that also carries a device id
|
|
105
|
+
* (`401 token error`) — so those use {@link baseHeaders} directly.
|
|
106
|
+
*/
|
|
107
|
+
private authHeaders;
|
|
108
|
+
/** Base headers common to every Solix request. */
|
|
109
|
+
private baseHeaders;
|
|
110
|
+
/** One request path for every Solix call (GET or POST) — always parses through the non-JSON guard. */
|
|
111
|
+
private send;
|
|
112
|
+
/** POST helper for the login/key-exchange path (which builds its own bespoke headers per request). */
|
|
113
|
+
private post;
|
|
114
|
+
/** Resolve the regional API host via domain-estimate (best-effort; keeps the default on failure). */
|
|
115
|
+
private estimateHost;
|
|
116
|
+
/** Do the localKey-bootstrapped ECDH key exchange and return the negotiated session key. */
|
|
117
|
+
private keyExchange;
|
|
118
|
+
/** Build the encrypted, signed `/passport/login` request body + headers for the negotiated key. */
|
|
119
|
+
private postLogin;
|
|
120
|
+
/**
|
|
121
|
+
* Turn a decrypted `/passport/login` payload into an `ok`/`2fa` result, establishing the session on
|
|
122
|
+
* `ok`. The passport marks a pending 2FA with a non-empty `fa_info.info`, and empties it once the code
|
|
123
|
+
* has been satisfied.
|
|
124
|
+
*/
|
|
125
|
+
private classifyLogin;
|
|
126
|
+
/** Decrypt a login envelope's `data` (base64 `IV(16)||AES-128-CBC`, keyed by the share key). */
|
|
127
|
+
private decryptLogin;
|
|
128
|
+
/**
|
|
129
|
+
* Authenticate with the account credentials. Resolves to `ok` with a {@link SolixSession}, or `2fa`
|
|
130
|
+
* when the passport sent a code — then call {@link submitVerifyCode}. A session that is already fresh
|
|
131
|
+
* (adopted from a store) is answered without a handshake.
|
|
132
|
+
*/
|
|
133
|
+
login(): Promise<SolixLoginResult>;
|
|
134
|
+
/** Complete a `2fa` login with the code the passport sent. */
|
|
135
|
+
submitVerifyCode(code: string): Promise<SolixLoginResult>;
|
|
136
|
+
/**
|
|
137
|
+
* One authenticated PLAIN read for both GET and POST endpoints (no per-request encryption; carries
|
|
138
|
+
* the auth token + `gtoken` only). Routes through {@link send} so every read keeps the non-JSON guard.
|
|
139
|
+
*/
|
|
140
|
+
private authed;
|
|
141
|
+
/**
|
|
142
|
+
* The account's bound Solix devices (flat list; may be empty when devices live under sites). The
|
|
143
|
+
* gateway's JSON is asserted to {@link SolixDeviceRecord} here, at the one trust boundary — every field
|
|
144
|
+
* beyond `device_sn`/`product_code` is optional on the record, so a caller reads them defensively.
|
|
145
|
+
*/
|
|
146
|
+
getDevices(): Promise<SolixDeviceRecord[]>;
|
|
147
|
+
/** The account's sites (systems); devices are typically grouped under a site. */
|
|
148
|
+
getSites(): Promise<unknown[]>;
|
|
149
|
+
/** Per-user AWS-IoT MQTT credentials (cert/key/endpoint/thing) for the real-time device plane. */
|
|
150
|
+
getUserMqttInfo(): Promise<SecureMqttCredentials>;
|
|
151
|
+
/**
|
|
152
|
+
* The pairable-product catalog (categories → products). This is Anker's product registry, not the
|
|
153
|
+
* account's devices — fetch it to label a discovered device's model code with a marketing name and
|
|
154
|
+
* category. Pair with {@link buildModelIndex}. It is a live endpoint, so it stays current without a
|
|
155
|
+
* baked-in table.
|
|
156
|
+
*/
|
|
157
|
+
getProductCatalog(): Promise<SolixProductCategory[]>;
|
|
158
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anker "Solix" cloud endpoints + app-line constants for {@link SolixClient}.
|
|
3
|
+
*
|
|
4
|
+
* Solix runs the SAME `algo_ecdh` passport as the eufy_mega account stack, re-skinned under a
|
|
5
|
+
* different `app-name` with its own API host and key-exchange bootstrap key (the bootstrap key lives
|
|
6
|
+
* in `core` beside its siblings — {@link SOLIX_LOCAL_KEY_HEX}). One Anker/eufy account logs in here
|
|
7
|
+
* with the exact login handshake the eufy client uses; only these constants differ. Authenticated
|
|
8
|
+
* resource reads, by contrast, are PLAIN JSON carrying just the auth token + `gtoken`.
|
|
9
|
+
*/
|
|
10
|
+
/** The `app-name` header value that scopes the passport + API to the Solix product. */
|
|
11
|
+
export declare const SOLIX_APP_NAME = "anker_power";
|
|
12
|
+
/** Domain-estimate bootstrap host. `POST /passport/estimate_domain {ab,mode:1}` answers the shard host. */
|
|
13
|
+
export declare const SOLIX_ESTIMATE_HOST = "uniapp-api-pr.anker.com";
|
|
14
|
+
/** EU-shard API host — the estimate result, and the fallback when estimate is skipped. */
|
|
15
|
+
export declare const SOLIX_DEFAULT_API_HOST = "ankerpower-api-eu.anker.com";
|
|
16
|
+
/** Solix cloud paths used by {@link SolixClient}. */
|
|
17
|
+
export declare const SOLIX_ENDPOINTS: {
|
|
18
|
+
readonly estimateDomain: "/passport/estimate_domain";
|
|
19
|
+
readonly keyExchange: "/openapi/oauth/key/exchange";
|
|
20
|
+
readonly login: "/passport/login";
|
|
21
|
+
/** Bound devices for the account (flat list). */
|
|
22
|
+
readonly getRelateAndBindDevices: "/power_service/v1/app/get_relate_and_bind_devices";
|
|
23
|
+
/** Sites (systems) the account owns; devices are grouped under a site. */
|
|
24
|
+
readonly getSiteList: "/power_service/v1/site/get_site_list";
|
|
25
|
+
/** Per-user AWS-IoT MQTT credentials (cert/key/endpoint/thing) for the real-time device plane. */
|
|
26
|
+
readonly getUserMqttInfo: "/v1/openapi/devicemanage/get_user_mqtt_info";
|
|
27
|
+
/** GET: the pairable-product catalog (categories → products), for labelling model codes. */
|
|
28
|
+
readonly productCategories: "/power_service/v1/product_categories";
|
|
29
|
+
};
|
|
@@ -3,3 +3,5 @@ export * from "./topics.js";
|
|
|
3
3
|
export * from "./app-client-id.js";
|
|
4
4
|
export * from "./broker-discovery.js";
|
|
5
5
|
export * from "./biz-stream.js";
|
|
6
|
+
export { SolixMqtt } from "./solix-mqtt.js";
|
|
7
|
+
export type { SolixMqttOptions, SolixMqttDevice, SolixReading, SolixParamFrame, SolixChannel } from "./solix-mqtt.js";
|
|
@@ -93,12 +93,25 @@ export declare class SecureMqtt extends EventEmitter implements RealtimeTranspor
|
|
|
93
93
|
* four topics for `eufy_life`).
|
|
94
94
|
*
|
|
95
95
|
* The grants are INSPECTED, not assumed: AWS IoT answers a policy-denied filter with a
|
|
96
|
-
*
|
|
96
|
+
* SUBACK_FAILURE (`0x80`) grant rather than failing the SUBSCRIBE, so subscribing with a credential
|
|
97
97
|
* whose scope doesn't cover the topic looks identical to success and then delivers nothing. A denied
|
|
98
98
|
* topic is reported via `error` naming the credential scope; only an all-denied device throws, so a
|
|
99
99
|
* line that grants its state channel but refuses (say) the OTA leg still works.
|
|
100
100
|
*/
|
|
101
101
|
subscribeDevice(device: EufyDevice): Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* Subscribe to explicit topic filters, returning the topics that were granted. A scope-denied filter
|
|
104
|
+
* comes back with SUBACK_FAILURE rather than an error (AWS IoT quirk), so it is dropped from the result
|
|
105
|
+
* instead of throwing — callers that need every leg check the returned list. Used by lines whose topic
|
|
106
|
+
* vocabulary isn't the eufy `subscribeTopics` shape (e.g. Anker Solix `dt/{app}/{pn}/{sn}`).
|
|
107
|
+
*/
|
|
108
|
+
subscribe(topics: string[]): Promise<string[]>;
|
|
109
|
+
/**
|
|
110
|
+
* Split SUBACK grants into granted vs scope-denied topics. AWS IoT marks a policy-denied filter with a
|
|
111
|
+
* SUBACK_FAILURE (`0x80`) grant rather than failing the SUBSCRIBE, so the two subscribe paths share
|
|
112
|
+
* this split and layer their own policy (drop vs report) on top.
|
|
113
|
+
*/
|
|
114
|
+
private partitionGrants;
|
|
102
115
|
/**
|
|
103
116
|
* Publish a raw payload to an MQTT topic (the command leg — `cmd/{app}/{pn}/{sn}/req`). The `body`
|
|
104
117
|
* is a pre-built envelope the caller supplies (the command router builds it). QoS 1 by default (the
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live telemetry for Anker Solix devices over the AWS-IoT MQTT plane.
|
|
3
|
+
*
|
|
4
|
+
* The transport is the shared `SecureMqtt` — the exact same anker AWS-IoT broker + per-user
|
|
5
|
+
* client-cert mutual TLS the eufy device path uses; a Solix account's `get_user_mqtt_info` result maps
|
|
6
|
+
* straight onto {@link SecureMqttCredentials}. Solix devices publish telemetry continuously on
|
|
7
|
+
* `dt/{app_name}/{product_code}/{device_sn}/param_info` as an **ff09 TLV frame** (the same framing
|
|
8
|
+
* family as {@link parseFf09SettingsResponse}), so this module only adds the Solix topic + a small
|
|
9
|
+
* ff09 param decoder on top of the reused transport.
|
|
10
|
+
*
|
|
11
|
+
* Frame layout (observed on a Smart Meter Gen 2 / AE1X0):
|
|
12
|
+
* ff09 | len(u16 LE, incl. trailing XOR checksum) | 5-byte header | TLV fields | xor
|
|
13
|
+
* each TLV field is `tag(1) | len(1) | value(len)`; measurement fields carry `type(1) | 4 bytes`,
|
|
14
|
+
* type `0x05` = float32 LE. Field `a2` is the device serial (ASCII after a leading type byte).
|
|
15
|
+
*/
|
|
16
|
+
import { EventEmitter } from "node:events";
|
|
17
|
+
import { type SecureMqttCredentials } from "./secure-mqtt.js";
|
|
18
|
+
import { type Logger } from "../../core/index.js";
|
|
19
|
+
/** A decoded telemetry channel: the raw value plus float/uint interpretations of a 4-byte payload. */
|
|
20
|
+
export interface SolixChannel {
|
|
21
|
+
/** The leading type byte (`0x05` = float32 LE for the meter's measurement channels). */
|
|
22
|
+
type: number;
|
|
23
|
+
raw: Buffer;
|
|
24
|
+
/** Present when the payload is 4 bytes: little-endian float32. */
|
|
25
|
+
float?: number;
|
|
26
|
+
/** Present when the payload is 4 bytes: little-endian uint32. */
|
|
27
|
+
uint?: number;
|
|
28
|
+
}
|
|
29
|
+
/** A parsed ff09 param frame: the device serial (from `a2`) + the raw TLV field map keyed by tag. */
|
|
30
|
+
export interface SolixParamFrame {
|
|
31
|
+
deviceSn?: string;
|
|
32
|
+
/** tag byte → value bytes (still including the per-field leading type byte for measurement fields). */
|
|
33
|
+
fields: Map<number, Buffer>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Telemetry field tags for the Smart Meter (AE1X0) that we emit under a stable NAME, keyed by ff09 tag
|
|
37
|
+
* byte. Only tags whose tag→name binding is CONFIRMED against a live frame live here:
|
|
38
|
+
*
|
|
39
|
+
* - `0xac` = `meterVoltageL1` — confirmed against live single-phase data (a nominal mains voltage).
|
|
40
|
+
*
|
|
41
|
+
* Every other measurement tag still surfaces as `channel_<hex tag>` (see {@link solixReadings}), so
|
|
42
|
+
* nothing on the wire is lost — a caller reads unconfirmed tags there. The names are deliberately NOT
|
|
43
|
+
* asserted for the rest: the app exposes the field *list*, but the tag→name *binding* below is a
|
|
44
|
+
* structural inference until a known-load capture pins it, and a mislabelled live float is worse than an
|
|
45
|
+
* honest `channel_<tag>`. The recovered candidates, to re-add one line each (moving the tag from this
|
|
46
|
+
* comment to the map above) as a known-load capture confirms each binding:
|
|
47
|
+
*
|
|
48
|
+
* 0xa8 meterPowerL1 0xa9 meterPowerL2 0xaa meterPowerL3 0xab meterPowerTotal
|
|
49
|
+
* 0xad meterVoltageL2 0xae meterVoltageL3 0xaf meterCurrentL1 0xb0 meterCurrentL2
|
|
50
|
+
* 0xb1 meterCurrentL3 0xb2 meterCurrentTotal 0xb3 meterImportEnergy 0xb4 meterExportEnergy
|
|
51
|
+
*/
|
|
52
|
+
export declare const SOLIX_METER_FIELD_NAMES: Readonly<Record<number, string>>;
|
|
53
|
+
/** Interpret one TLV value as a telemetry channel (leading type byte + payload). */
|
|
54
|
+
export declare function readSolixChannel(value: Buffer | undefined): SolixChannel | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Decode an ff09 Solix param frame into its serial + TLV field map. Returns `null` for a non-ff09
|
|
57
|
+
* buffer, a length field that doesn't fit, or a bad checksum. Validates the trailing XOR checksum first
|
|
58
|
+
* (so a corrupted frame is rejected rather than yielding plausible floats), then walks `tag|len|value`
|
|
59
|
+
* from the first `0xa1` tag to the declared length minus the checksum byte via the shared
|
|
60
|
+
* `walkFf09Tlv` (bounded by `end`, so a field length can't overrun into the checksum).
|
|
61
|
+
*/
|
|
62
|
+
export declare function decodeSolixParamFrame(buf: Buffer): SolixParamFrame | null;
|
|
63
|
+
/**
|
|
64
|
+
* Reduce a param frame to named + raw telemetry values. Tags below `0xa6` are skipped — `a1`/`a2`/`a3`
|
|
65
|
+
* carry the field count, the serial and the status, not measurements. A measurement channel is one whose
|
|
66
|
+
* leading type byte is `0x05` (float32 LE over a 4-byte payload); any other type is a non-measurement
|
|
67
|
+
* param and contributes nothing. Each measurement is emitted under `channel_<hex tag>`, and additionally
|
|
68
|
+
* under its name when the tag has a confirmed one in {@link SOLIX_METER_FIELD_NAMES}.
|
|
69
|
+
*/
|
|
70
|
+
export declare function solixReadings(frame: SolixParamFrame): Record<string, number>;
|
|
71
|
+
/** A live telemetry sample emitted by {@link SolixMqtt} as a `reading` event. */
|
|
72
|
+
export interface SolixReading {
|
|
73
|
+
deviceSn: string;
|
|
74
|
+
productCode: string;
|
|
75
|
+
topic: string;
|
|
76
|
+
frame: SolixParamFrame;
|
|
77
|
+
values: Record<string, number>;
|
|
78
|
+
}
|
|
79
|
+
/** The minimum device shape {@link SolixMqtt.watch} needs (as returned by `SolixClient.getDevices`). */
|
|
80
|
+
export interface SolixMqttDevice {
|
|
81
|
+
device_sn: string;
|
|
82
|
+
product_code: string;
|
|
83
|
+
}
|
|
84
|
+
/** Options for {@link SolixMqtt}. */
|
|
85
|
+
export interface SolixMqttOptions {
|
|
86
|
+
/** `get_user_mqtt_info` result — carries endpoint, cert/key, app_name, thing_name, user_id. */
|
|
87
|
+
mqttInfo: SecureMqttCredentials;
|
|
88
|
+
/** Override the MQTT clientId. Defaults to the cert CN (`thing_name`), distinct from the app's id. */
|
|
89
|
+
clientId?: string;
|
|
90
|
+
/**
|
|
91
|
+
* Account/user id (40-hex) for the arming `account_id` + heartbeat topic. Defaults to
|
|
92
|
+
* `mqttInfo.user_id`; set it if the credentials omit it.
|
|
93
|
+
*/
|
|
94
|
+
userId?: string;
|
|
95
|
+
/**
|
|
96
|
+
* How often (ms) to re-send the device-info arming request that keeps realtime telemetry flowing.
|
|
97
|
+
* The device stops pushing `param_info` when no client keeps requesting it (the app re-arms on every
|
|
98
|
+
* foreground resume + a periodic heartbeat), so a passive subscriber goes silent after the server's
|
|
99
|
+
* reporting window closes. Default 25s — inside the observed ~30s cadence with keepalive 60. Set `0`
|
|
100
|
+
* to disable arming (subscribe-only, the old behaviour).
|
|
101
|
+
*/
|
|
102
|
+
armIntervalMs?: number;
|
|
103
|
+
/**
|
|
104
|
+
* The `head.client_id` stamped into the command/heartbeat envelopes — the app-shaped
|
|
105
|
+
* `android-{app_name}-{user_id}-{mqttUuid}-{ts}` (see {@link buildAppShapedClientId}). Defaults to
|
|
106
|
+
* that shape built from {@link mqttUuid}. Pass this to pin the whole string.
|
|
107
|
+
*/
|
|
108
|
+
appClientId?: string;
|
|
109
|
+
/**
|
|
110
|
+
* Stable 16-hex install UUID for the app-shaped client id. Defaults to one derived deterministically
|
|
111
|
+
* from the user id ({@link mqttUuidFrom}) — no storage needed, so the broker sees one stable client
|
|
112
|
+
* across restarts.
|
|
113
|
+
*
|
|
114
|
+
* The trade this makes: the default seed is the **account** id, which every client on that account
|
|
115
|
+
* shares, so two clients on one account derive the same uuid → the same `client_id`, and the broker
|
|
116
|
+
* evicts one to admit the other (they take the channel from each other indefinitely). Restart
|
|
117
|
+
* stability is the common case and this is the deliberate default, but pass an explicit `mqttUuid`
|
|
118
|
+
* (per host/install) when more than one client runs on the same account, to be told apart.
|
|
119
|
+
*/
|
|
120
|
+
mqttUuid?: string;
|
|
121
|
+
/** The account's `site_id` for the `power_site` heartbeat. Omitted from the frame when unknown. */
|
|
122
|
+
siteId?: string;
|
|
123
|
+
logger?: Logger;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Subscribe to a Solix device's live telemetry and emit decoded `reading` events. Reuses
|
|
127
|
+
* `SecureMqtt` for the connection; adds only the Solix data topic + ff09 param decoding.
|
|
128
|
+
*
|
|
129
|
+
* const mqtt = new SolixMqtt({ mqttInfo: await solix.getUserMqttInfo() });
|
|
130
|
+
* mqtt.on("reading", (r) => console.log(r.deviceSn, r.values.meterVoltageL1));
|
|
131
|
+
* await mqtt.watch(device); // device = a SolixClient.getDevices() entry
|
|
132
|
+
*/
|
|
133
|
+
export declare class SolixMqtt extends EventEmitter {
|
|
134
|
+
private readonly transport;
|
|
135
|
+
private readonly appName;
|
|
136
|
+
private readonly userId?;
|
|
137
|
+
private readonly appClientId;
|
|
138
|
+
private readonly armIntervalMs;
|
|
139
|
+
private readonly logger?;
|
|
140
|
+
private readonly siteId?;
|
|
141
|
+
private readonly watched;
|
|
142
|
+
private seq;
|
|
143
|
+
private armTimer?;
|
|
144
|
+
/**
|
|
145
|
+
* Bind to one account's MQTT plane. The envelope `client_id` takes the app's shape
|
|
146
|
+
* (`android-{app}-{uid}-{mqttUuid}-{ts}`); its `mqttUuid` half must be stable across restarts, or every
|
|
147
|
+
* restart presents itself to the broker as a new client, so it defaults deterministically from the user
|
|
148
|
+
* id (see {@link SolixMqttOptions.mqttUuid}) rather than a fresh random per instance.
|
|
149
|
+
*/
|
|
150
|
+
constructor(opts: SolixMqttOptions);
|
|
151
|
+
/**
|
|
152
|
+
* Connect, subscribe to the device's telemetry (+ command-reply) topics, ARM realtime reporting, and
|
|
153
|
+
* start the re-arm/heartbeat timer so telemetry keeps flowing without the app. Idempotent per device.
|
|
154
|
+
*
|
|
155
|
+
* Subscribes ONLY to what the device sends — `param_info` plus the device and account command-reply
|
|
156
|
+
* channels — never the `…/req` channels, which are the app→device request side this arms on, and would
|
|
157
|
+
* echo its own publishes back.
|
|
158
|
+
*
|
|
159
|
+
* Throws when `param_info` was not granted. A scope-denied filter comes back as SUBACK_FAILURE rather
|
|
160
|
+
* than an error (see `SecureMqtt.subscribe`), so an unusable subscription otherwise looks like
|
|
161
|
+
* success: the call would resolve and arm on every interval while no reading ever arrives.
|
|
162
|
+
*
|
|
163
|
+
* The re-arm timer is unreffed, so a caller that watches and returns can still exit.
|
|
164
|
+
*/
|
|
165
|
+
watch(device: SolixMqttDevice): Promise<void>;
|
|
166
|
+
/** Tear down the connection and stop the re-arm timer. */
|
|
167
|
+
close(): Promise<void>;
|
|
168
|
+
/**
|
|
169
|
+
* Re-arm every watched device and send the site heartbeat. The device only pushes `param_info` while
|
|
170
|
+
* a client keeps requesting it — this replays the app's `requestDeviceInfo` (cmd 17) + `power_site`
|
|
171
|
+
* heartbeat (cmd 10); the request frames are reproduced byte-for-byte by {@link buildFf09Request}
|
|
172
|
+
* (checksum-verified against captured frames in its spec). Best-effort: a publish failure is emitted,
|
|
173
|
+
* not thrown, so one bad device doesn't stop the rest or kill the timer.
|
|
174
|
+
*/
|
|
175
|
+
private armAll;
|
|
176
|
+
/** Publish the device-info arming request (both the "info" and "realtime" ff09 variants the app sends). */
|
|
177
|
+
private arm;
|
|
178
|
+
/** The common `head` fields for every cmd envelope; callers add `cmd` + the per-message variable bits. */
|
|
179
|
+
private makeHead;
|
|
180
|
+
/**
|
|
181
|
+
* Build the `{head, payload}` cmd-17 (requestDeviceInfo) envelope carrying a base64 ff09 request.
|
|
182
|
+
* `account_id` is omitted when the user id is unknown: a live broker cannot tell an empty placeholder
|
|
183
|
+
* from a real value, so sending `""` would claim an account this client does not have.
|
|
184
|
+
*/
|
|
185
|
+
private commandEnvelope;
|
|
186
|
+
/**
|
|
187
|
+
* The `power_site` heartbeat (cmd 10) envelope the app sends on a timer to keep the session alive.
|
|
188
|
+
* `site_id` is omitted when unknown, for the same reason `account_id` is in {@link commandEnvelope}.
|
|
189
|
+
*/
|
|
190
|
+
private heartbeatEnvelope;
|
|
191
|
+
/**
|
|
192
|
+
* Decode one inbound MQTT message envelope and emit a `reading` if it carries an ff09 param frame. The
|
|
193
|
+
* product code and the fallback serial come from the topic (`dt/{app}/{pn}/{sn}/param_info`); the frame's
|
|
194
|
+
* own `a2` field wins for the serial when it carries one.
|
|
195
|
+
*/
|
|
196
|
+
private onMessage;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Pull the ff09 binary frame out of a received message. Solix telemetry arrives as a `{head, payload}`
|
|
200
|
+
* envelope whose `payload` is a JSON string carrying base64 `data` (or `trans`); `SecureMqtt`
|
|
201
|
+
* has already JSON-parsed the outer envelope. Returns the decoded frame bytes, or `null`.
|
|
202
|
+
*/
|
|
203
|
+
export declare function extractFf09Payload(raw: unknown): Buffer | null;
|
|
204
|
+
/**
|
|
205
|
+
* Build the ff09 request frame the app base64-encodes into a `requestDeviceInfo` (cmd 17) command's
|
|
206
|
+
* `data`. Captured live from the Anker app — request-type tag `a1`=0x22; the `realtime` variant adds
|
|
207
|
+
* `a2`/`a3` params (this is the one that keeps `param_info` reporting flowing), while `info` is the bare
|
|
208
|
+
* device-info fetch. Frame:
|
|
209
|
+
* `ff09 | len(u16 LE, TOTAL bytes incl. ff09+len+xor) | 5-byte header | a1 01 22
|
|
210
|
+
* [| a2 02 01 01 | a3 03 02 2c 01] | fe … <ts32 LE> | xor`
|
|
211
|
+
* `fe` carries a fresh unix-timestamp nonce; the trailing byte is XOR of every preceding byte (the same
|
|
212
|
+
* checksum the meter's telemetry frames use — verified to reproduce the captured frames exactly).
|
|
213
|
+
*/
|
|
214
|
+
export declare function buildFf09Request(variant: "info" | "realtime", atUnixSec?: number): Buffer;
|
|
@@ -78,3 +78,23 @@ export interface ParsedTopic {
|
|
|
78
78
|
* so an unparsed topic leaves `deviceSn` unset instead of carrying a guess.
|
|
79
79
|
*/
|
|
80
80
|
export declare function parseSecureTopic(topic: string): ParsedTopic | undefined;
|
|
81
|
+
/** The per-device Solix topics for `{appName, productCode, deviceSn}`. */
|
|
82
|
+
export interface SolixDeviceTopics {
|
|
83
|
+
/** Telemetry the device pushes (SUBSCRIBE) — ff09 `param_info` frames. */
|
|
84
|
+
paramInfo: string;
|
|
85
|
+
/** This device's command replies (SUBSCRIBE). */
|
|
86
|
+
cmdRes: string;
|
|
87
|
+
/** The device's requestDeviceInfo channel (PUBLISH only — the app arms reporting here). */
|
|
88
|
+
req: string;
|
|
89
|
+
}
|
|
90
|
+
/** Build the per-device Solix topics. `param_info` is the telemetry we decode; `req` is publish-only. */
|
|
91
|
+
export declare function solixDeviceTopics(appName: string, productCode: string, deviceSn: string): SolixDeviceTopics;
|
|
92
|
+
/** The per-account Solix topics keyed by `user_id`. */
|
|
93
|
+
export interface SolixUserTopics {
|
|
94
|
+
/** Account-level command replies (SUBSCRIBE). */
|
|
95
|
+
cmdRes: string;
|
|
96
|
+
/** The `power_site` heartbeat channel (PUBLISH only). */
|
|
97
|
+
powerSite: string;
|
|
98
|
+
}
|
|
99
|
+
/** Build the per-account Solix topics. Note: the account `…/req` channel is publish-side and NOT subscribed. */
|
|
100
|
+
export declare function solixUserTopics(appName: string, userId: string): SolixUserTopics;
|
|
@@ -18,6 +18,23 @@ import type { FfmpegLevel } from "../ffmpeg.js";
|
|
|
18
18
|
import { SharedLiveSource } from "./shared-live-source.js";
|
|
19
19
|
import { type PowerTier, type SessionManagerOpts } from "./session-manager.js";
|
|
20
20
|
import { FragmentRecording } from "./fragment-recording.js";
|
|
21
|
+
/**
|
|
22
|
+
* What a caller's own deadline on a station call has to clear, in milliseconds.
|
|
23
|
+
*
|
|
24
|
+
* A caller that bounds one of these calls itself races these waits, and a bound below them reports the
|
|
25
|
+
* caller's own expiry in place of the reason this SDK was about to give — the two are indistinguishable to
|
|
26
|
+
* whoever reads the outcome, and they call for different next steps. Published so that bound can be derived
|
|
27
|
+
* rather than copied: a literal in a caller's source is a second source of truth that goes stale silently
|
|
28
|
+
* when these change.
|
|
29
|
+
*
|
|
30
|
+
* `connect` applies to every call on a station, because nothing can be addressed to one before its session is
|
|
31
|
+
* up. `level2Grace` applies twice where the key is required: the negotiation is re-prompted once.
|
|
32
|
+
*/
|
|
33
|
+
export declare const P2P_STATION_WAITS: {
|
|
34
|
+
readonly connect: 20000;
|
|
35
|
+
readonly level2Grace: 25000;
|
|
36
|
+
readonly level2Settle: 8000;
|
|
37
|
+
};
|
|
21
38
|
/**
|
|
22
39
|
* Options accepted when warming a {@link SharedLiveSource} for a device (all optional).
|
|
23
40
|
*
|
|
@@ -90,6 +107,12 @@ export declare class P2PCommandRouter {
|
|
|
90
107
|
constructor(deps: P2PRouterDeps);
|
|
91
108
|
/** Forward one P2P failure once even when both the session listener and startup waiter observe it. */
|
|
92
109
|
private reportError;
|
|
110
|
+
/**
|
|
111
|
+
* Emit a live trace under a station session's handle, for work this router does ON that session before
|
|
112
|
+
* the session itself records anything — reaching the station, and resolving what a device is on it. Same
|
|
113
|
+
* handle as everything the session goes on to trace, which is what groups one attempt.
|
|
114
|
+
*/
|
|
115
|
+
private traceOnStation;
|
|
93
116
|
/**
|
|
94
117
|
* Whether this transport stack drives `dev`'s `ff09-*` commands — true when the device has its own
|
|
95
118
|
* usable P2P endpoint (a non-empty `p2p_did`). The command sink asks each stack this to route a
|