@mega-yfue/eufy-sdk 0.2.0-beta.2 → 0.2.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 +16 -11
- package/dist/client/types.d.ts +6 -2
- package/dist/core/contracts.d.ts +57 -27
- 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 +115 -0
- package/dist/core/store.d.ts +38 -10
- package/dist/index.js +2713 -452
- 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/index.d.ts +18 -3
- 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 +21 -5
- package/dist/model/capabilities/vacuum-clean.d.ts +74 -25
- 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 +136 -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 +269 -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 +2 -0
- package/dist/transport/mqtt/secure-mqtt.d.ts +14 -1
- package/dist/transport/mqtt/solix-mqtt.d.ts +321 -0
- package/dist/transport/mqtt/topics.d.ts +30 -0
- package/dist/transport/p2p/command-router.d.ts +152 -15
- package/dist/transport/p2p/index.d.ts +1 -0
- package/dist/transport/p2p/live-stream.d.ts +5 -4
- package/dist/transport/p2p/live-trace.d.ts +118 -6
- package/dist/transport/p2p/media.d.ts +11 -0
- package/dist/transport/p2p/p2p-session.d.ts +13 -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/stored-image-cache.d.ts +7 -1
- package/package.json +3 -2
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* Cloud APIs: the eufy v6 cloud (+ legacy, planned)
|
|
5
5
|
* Realtime: secure MQTT (appliances) + P2P (cameras/HomeBases)
|
|
6
6
|
*
|
|
7
|
-
* const eufy = new EufyMega({ email, password, region: "eu" });
|
|
7
|
+
* const eufy = new EufyMega({ email, password, region: "eu-pr" });
|
|
8
8
|
* await eufy.login(); // → LoginResult; on success the SDK auto-starts realtime (push/MQTT/wired P2P)
|
|
9
9
|
* eufy.on("motion", (e) => console.log(e.deviceSn)); // typed semantic events — flowing already
|
|
10
10
|
* const dev = await eufy.getDevice((await eufy.getDevices())[0].sn);
|
|
11
|
-
* await dev.camera()?.snapshotStored();
|
|
11
|
+
* await dev.camera?.()?.snapshotStored?.();
|
|
12
12
|
*
|
|
13
13
|
* Connectivity is SDK-managed: the host calls no `connect*`. P2P to a battery camera is opened only
|
|
14
14
|
* when a command/stream/doorbell-ring needs it and closed when idle, so the camera can sleep.
|
|
@@ -346,8 +346,8 @@ export declare class EufyMega extends EventEmitter {
|
|
|
346
346
|
* @example
|
|
347
347
|
* ```ts
|
|
348
348
|
* const res = await eufy.login();
|
|
349
|
-
* if (res.status === "captcha") await eufy.solveCaptcha(await
|
|
350
|
-
* else if (res.status === "2fa") await eufy.submitVerifyCode(await
|
|
349
|
+
* if (res.status === "captcha") await eufy.solveCaptcha(await promptUser(res.image));
|
|
350
|
+
* else if (res.status === "2fa") await eufy.submitVerifyCode(await promptUser());
|
|
351
351
|
* ```
|
|
352
352
|
*/
|
|
353
353
|
login(opts?: {
|
|
@@ -393,9 +393,9 @@ export declare class EufyMega extends EventEmitter {
|
|
|
393
393
|
/**
|
|
394
394
|
* Combine explicit P2P media with the optional passive push-thumbnail provider.
|
|
395
395
|
*
|
|
396
|
-
* The retained still also becomes the answer for a live still that could not be captured.
|
|
397
|
-
* serves one camera at a time and a live view outranks a tile
|
|
398
|
-
* being watched is refused at the transport. Answering the retained bytes answers the read rather than
|
|
396
|
+
* The retained still also becomes the answer for a live still that could not be captured. One session
|
|
397
|
+
* serves one camera at a time and a live view outranks a tile — a still does not open a connection of its
|
|
398
|
+
* own — so a still asked for while a sibling is being watched is refused at the transport. Answering the retained bytes answers the read rather than
|
|
399
399
|
* failing it, marked {@link MediaProvider.snapshotLive} `retained` so the caller knows they are not
|
|
400
400
|
* current. With nothing retained the refusal stands.
|
|
401
401
|
*/
|
|
@@ -493,7 +493,7 @@ export declare class EufyMega extends EventEmitter {
|
|
|
493
493
|
* @example
|
|
494
494
|
* ```ts
|
|
495
495
|
* const dev = await eufy.getDevice(sn);
|
|
496
|
-
* if (dev.has("camera")) await dev.camera()?.snapshotStored();
|
|
496
|
+
* if (dev.has("camera")) await dev.camera?.()?.snapshotStored?.();
|
|
497
497
|
* console.log(dev.getProperty("battery"));
|
|
498
498
|
* ```
|
|
499
499
|
*/
|
|
@@ -752,10 +752,15 @@ export declare class EufyMega extends EventEmitter {
|
|
|
752
752
|
*/
|
|
753
753
|
private sendRealtimeInit;
|
|
754
754
|
/**
|
|
755
|
-
*
|
|
755
|
+
* The open P2P sessions, by key. P2P is auto-managed: wired stations are warmed at login, battery
|
|
756
756
|
* stations open on demand (command / stream, or an opted-in event pre-warm) and idle-detach — so this
|
|
757
|
-
* map grows and shrinks over time.
|
|
758
|
-
*
|
|
757
|
+
* map grows and shrinks over time.
|
|
758
|
+
*
|
|
759
|
+
* A station's own session is keyed by its serial, and `p2pConnect(stationSn)` / `p2pClose(stationSn)`
|
|
760
|
+
* track those. A station serving more than one camera at once also holds a session per extra camera,
|
|
761
|
+
* keyed `<stationSn>#live:<channel>` — these carry media alone and raise no connection events, because
|
|
762
|
+
* a station announces its state to every client that connects and reporting each copy would double
|
|
763
|
+
* every event the station's own session already delivers.
|
|
759
764
|
*/
|
|
760
765
|
getP2pSessions(): Map<string, P2PSession>;
|
|
761
766
|
/**
|
package/dist/client/types.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `interface EufyMega` (the typed on/once/off/emit overloads) stays in `eufy-mega.ts` next to the
|
|
6
6
|
* class — TS declaration merging requires both in the same module.
|
|
7
7
|
*/
|
|
8
|
-
import type { MegaClientConfig } from "../transport/http/mega-client.js";
|
|
8
|
+
import type { MegaClientConfig, SessionExpiredError } from "../transport/http/mega-client.js";
|
|
9
9
|
import type { FcmStore } from "../transport/push/store.js";
|
|
10
10
|
import type { FfmpegLevel } from "../transport/ffmpeg.js";
|
|
11
11
|
import type { DeviceEventMap } from "../model/capabilities/index.js";
|
|
@@ -375,8 +375,12 @@ export type EufyMegaEventMap = {
|
|
|
375
375
|
* token expired. The SDK has already cleared the persisted session, so recovery is a fresh `login()`
|
|
376
376
|
* (which usually needs 2FA). Distinct from `error`: a session error is emitted ONLY here, not also
|
|
377
377
|
* on `error`.
|
|
378
|
+
*
|
|
379
|
+
* The error carries the rate: `err.retryAfterMs` is how long the next session replacement is barred
|
|
380
|
+
* for, and `err.contended` says this session is being displaced by another client rather than expiring
|
|
381
|
+
* — which a re-login does not answer. A login made before that wait elapses extends it.
|
|
378
382
|
*/
|
|
379
|
-
sessionExpired: [err:
|
|
383
|
+
sessionExpired: [err: SessionExpiredError];
|
|
380
384
|
error: [err: Error];
|
|
381
385
|
};
|
|
382
386
|
/** Event names {@link EufyMega} can emit. */
|
package/dist/core/contracts.d.ts
CHANGED
|
@@ -75,28 +75,57 @@ export declare class CameraDisabledError extends Error {
|
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
77
|
/**
|
|
78
|
-
*
|
|
78
|
+
* Work on a station was refused: the station did not provide the session key that work requires.
|
|
79
79
|
*
|
|
80
|
-
* A station
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
80
|
+
* A station reached over its HomeBase encrypts what it is sent under a key negotiated once per connection, and
|
|
81
|
+
* a media start for an attached camera has no unencrypted form at all — so without that key there is nothing
|
|
82
|
+
* to send, however reachable the station is. Naming this apart from a source that failed is what separates an
|
|
83
|
+
* account whose cipher material could not be resolved from a camera that is off, a station that is busy, or a
|
|
84
|
+
* stream that produced nothing: they share no next step.
|
|
85
85
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
86
|
+
* The `level2-unavailable` trace states WHY the key is not coming. This states only that it is not, because
|
|
87
|
+
* that is what the refusal itself knows.
|
|
88
88
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
89
|
+
* `stationSn` is the station that owed the key, which is the parent for an attached camera and therefore not
|
|
90
|
+
* the serial the refused call was made about: several cameras refused at once are one station's outcome, and
|
|
91
|
+
* nothing else in the refusal says so.
|
|
91
92
|
*/
|
|
92
|
-
export declare class
|
|
93
|
-
/** The
|
|
94
|
-
readonly
|
|
95
|
-
/** Always true: the
|
|
93
|
+
export declare class StationKeyUnavailableError extends Error {
|
|
94
|
+
/** The station whose session key did not arrive. */
|
|
95
|
+
readonly stationSn: string;
|
|
96
|
+
/** Always true: the negotiation is per connection, so a later one may still produce a key. */
|
|
96
97
|
readonly retryable = true;
|
|
97
98
|
constructor(
|
|
98
|
-
/** The
|
|
99
|
-
|
|
99
|
+
/** The station whose session key did not arrive. */
|
|
100
|
+
stationSn: string, options?: {
|
|
101
|
+
cause?: unknown;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Work on a station was refused: its session did not connect within the wait it was given.
|
|
106
|
+
*
|
|
107
|
+
* A station is reached over its own session, and nothing addressed to it — a media start, a property read, a
|
|
108
|
+
* still — can be attempted before that session is up. Naming this apart from every other failure is what tells
|
|
109
|
+
* a station that could not be reached at all from one that answered and then refused, or one that served media
|
|
110
|
+
* a caller could not use: those call for opposite next steps, and a caller cannot infer which it had from a
|
|
111
|
+
* message.
|
|
112
|
+
*
|
|
113
|
+
* `waitedMs` is how long was actually waited, which a caller compares against its own deadline to know whether
|
|
114
|
+
* this SDK concluded or its own bound expired first. `stationSn` is the station that could not be reached —
|
|
115
|
+
* the parent for an attached camera, so it is not derivable from the serial the call was made about.
|
|
116
|
+
*/
|
|
117
|
+
export declare class StationUnreachableError extends Error {
|
|
118
|
+
/** The station whose session did not connect. */
|
|
119
|
+
readonly stationSn: string;
|
|
120
|
+
/** How long the session was waited on before this was raised. */
|
|
121
|
+
readonly waitedMs: number;
|
|
122
|
+
/** Always true: a station unreachable now may answer on a later attempt. */
|
|
123
|
+
readonly retryable = true;
|
|
124
|
+
constructor(
|
|
125
|
+
/** The station whose session did not connect. */
|
|
126
|
+
stationSn: string,
|
|
127
|
+
/** How long the session was waited on before this was raised. */
|
|
128
|
+
waitedMs: number, options?: {
|
|
100
129
|
cause?: unknown;
|
|
101
130
|
});
|
|
102
131
|
}
|
|
@@ -684,8 +713,9 @@ export interface MediaProvider {
|
|
|
684
713
|
/**
|
|
685
714
|
* Present and `true` only when these bytes are the RETAINED still rather than a fresh capture.
|
|
686
715
|
*
|
|
687
|
-
* A live still is refused while a sibling camera on the same station is being watched, because
|
|
688
|
-
*
|
|
716
|
+
* A live still is refused while a sibling camera on the same station is being watched, because one
|
|
717
|
+
* session serves one camera at a time, a still does not open a connection of its own, and the live view
|
|
718
|
+
* is the picture someone is looking at. Answering
|
|
689
719
|
* the retained still there answers the call instead of failing it, and this says the bytes are not
|
|
690
720
|
* current. Absent means freshly captured.
|
|
691
721
|
*/
|
|
@@ -694,18 +724,18 @@ export interface MediaProvider {
|
|
|
694
724
|
/**
|
|
695
725
|
* Open a managed live stream.
|
|
696
726
|
*
|
|
697
|
-
* Several cameras behind one station
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
* camera.
|
|
727
|
+
* Several cameras behind one station stream at the same time, each over its own connection to it. One
|
|
728
|
+
* connection serves one camera — a station answers the most recent start on a session, so two cameras
|
|
729
|
+
* sharing one take it from each other in turn — so a camera asked for while its station is already
|
|
730
|
+
* serving another gets a connection of its own. Measured on a base carrying two attached cameras, one
|
|
731
|
+
* at 3840x2160: both held full frame rate at once. Each handle receives only the frames the station
|
|
732
|
+
* tagged for ITS camera.
|
|
703
733
|
*
|
|
704
734
|
* @example
|
|
705
735
|
* ```ts
|
|
706
|
-
* const stream = await cam.live();
|
|
707
|
-
* stream
|
|
708
|
-
* stream
|
|
736
|
+
* const stream = await cam.live?.();
|
|
737
|
+
* stream?.on("video", (frame) => sink.write(frame.data)); // Annex-B
|
|
738
|
+
* stream?.stop(); // detach this consumer
|
|
709
739
|
* ```
|
|
710
740
|
*/
|
|
711
741
|
live(opts?: SharedSourceHints & AbortableCall & Record<string, unknown>): Promise<LiveStreamConsumer>;
|
package/dist/core/crypto.d.ts
CHANGED
|
@@ -35,6 +35,14 @@ export declare const EUFY_MEGA_LOCAL_KEY_HEX = "2500a7d5617812f9d52515b2c8f20a3d
|
|
|
35
35
|
* the mega localKey. Identified by HMAC-matching a captured eufylife key-exchange signature.
|
|
36
36
|
*/
|
|
37
37
|
export declare const EUFYLIFE_LOCAL_KEY_HEX = "118c12c81e211149304bd70a0c071d01";
|
|
38
|
+
/**
|
|
39
|
+
* The **Anker Solix** passport localKey — the AES-128 bootstrap key for the `anker_power` app-line
|
|
40
|
+
* (power stations / smart meter). Solix runs the SAME `algo_ecdh` passport as the eufy_mega stack,
|
|
41
|
+
* re-skinned under a different `app-name` + API host, so the login key-exchange wraps the ephemeral
|
|
42
|
+
* client public key with this key; distinct from {@link EUFY_MEGA_LOCAL_KEY_HEX}. Authenticated Solix
|
|
43
|
+
* reads carry only the token + `gtoken` (no per-request encryption).
|
|
44
|
+
*/
|
|
45
|
+
export declare const SOLIX_LOCAL_KEY_HEX = "e8ad18f61bbd3fbd52d5ed12d14d3b9c";
|
|
38
46
|
/**
|
|
39
47
|
* Hardcoded server P-256 public key (uncompressed 0x04||X||Y) used to encrypt
|
|
40
48
|
* the LOGIN password via a one-shot ECDH (separate from the per-session key).
|
|
@@ -44,6 +52,8 @@ export declare const SERVER_STATIC_PUBLIC_KEY_HEX = "04c5c00c4f8d1197cc7c3167c52
|
|
|
44
52
|
export declare function genId(): string;
|
|
45
53
|
/** Unix seconds as a string (X-Request-Ts). */
|
|
46
54
|
export declare function nowSec(): string;
|
|
55
|
+
/** md5 hex digest — the one place this derivation lives (gtoken, openudid seeds, …). */
|
|
56
|
+
export declare function md5Hex(input: string): string;
|
|
47
57
|
/** gtoken header = md5(user_id) hex. */
|
|
48
58
|
export declare function gtoken(userId: string): string;
|
|
49
59
|
/** 16-byte AES key = first half of the shared secret hex (shareKey[:16 bytes]). */
|
package/dist/core/index.d.ts
CHANGED
package/dist/core/logger.d.ts
CHANGED
|
@@ -19,7 +19,9 @@ export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
|
19
19
|
* ```ts
|
|
20
20
|
* // A custom sink (or pass a tslog / winston instance directly — they already match this shape):
|
|
21
21
|
* const eufy = new EufyMega({
|
|
22
|
-
*
|
|
22
|
+
* email,
|
|
23
|
+
* password,
|
|
24
|
+
* logger: { debug: (m, ...a) => myLog.debug(m, ...a), info: () => {}, warn: console.warn, error: console.error },
|
|
23
25
|
* });
|
|
24
26
|
* ```
|
|
25
27
|
*/
|
|
@@ -38,8 +40,8 @@ export declare const noopLogger: Logger;
|
|
|
38
40
|
*
|
|
39
41
|
* @example
|
|
40
42
|
* ```ts
|
|
41
|
-
* const eufy = new EufyMega({ logger: new ConsoleLogger() }); // verbose
|
|
42
|
-
* const quiet = new EufyMega({ logger: new ConsoleLogger("warn") }); // warn + error only
|
|
43
|
+
* const eufy = new EufyMega({ email, password, logger: new ConsoleLogger() }); // verbose
|
|
44
|
+
* const quiet = new EufyMega({ email, password, logger: new ConsoleLogger("warn") }); // warn + error only
|
|
43
45
|
* ```
|
|
44
46
|
*/
|
|
45
47
|
export declare class ConsoleLogger implements Logger {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anker Solix vendor-JSON record shapes — the cross-layer contract between the transport client (which
|
|
3
|
+
* RETURNS them off the wire) and the model layer (which resolves them into `SolixDevice`). They live in
|
|
4
|
+
* `core` for the same reason the command/media boundary does: the hard `transport ⊥ model` rule forbids
|
|
5
|
+
* either layer importing the other, so a type both need is neither's to own.
|
|
6
|
+
*
|
|
7
|
+
* @module core/solix-types
|
|
8
|
+
*/
|
|
9
|
+
/** A discovered Solix device record, as returned by `SolixClient.getDevices()`. */
|
|
10
|
+
export interface SolixDeviceRecord {
|
|
11
|
+
device_sn: string;
|
|
12
|
+
product_code: string;
|
|
13
|
+
device_name?: string;
|
|
14
|
+
alias_name?: string;
|
|
15
|
+
device_sw_version?: string;
|
|
16
|
+
wifi_online?: boolean;
|
|
17
|
+
wifi_name?: string;
|
|
18
|
+
rssi?: string | number;
|
|
19
|
+
[k: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* One battery discharge-cutoff (minimum-SOC) preset, as returned by
|
|
23
|
+
* `SolixClient.getPowerCutoff()`. `output_cutoff_data` is the minimum state-of-charge percent the
|
|
24
|
+
* option enforces; `id` is what `setPowerCutoff()` takes; `is_selected` (1) marks the current one.
|
|
25
|
+
*/
|
|
26
|
+
export interface SolixPowerCutoffOption {
|
|
27
|
+
id: number;
|
|
28
|
+
output_cutoff_data: number;
|
|
29
|
+
is_selected?: number;
|
|
30
|
+
[k: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The Solarbank's battery SOC-limit settings — the `param_data` block read from / written to
|
|
34
|
+
* `site/get_site_device_param` under `param_type "27"` (verified live on an AE103). All values are
|
|
35
|
+
* whole-percent integers. `chargeUpperLimit` caps charging (the app's "charge limit" / max SOC) and
|
|
36
|
+
* `dischargeLowerLimit` floors discharging (the "discharge limit" / minimum SOC — the same quantity the
|
|
37
|
+
* realtime `b5` telemetry blob reports). `backupReserve` (+ its switch) is the reserved-for-outage SOC;
|
|
38
|
+
* `socCalibrationEnable` is the periodic full-cycle calibration toggle. The wire keys are the
|
|
39
|
+
* snake_case form (`charge_upper_limit`, `discharge_lower_limit`, `backup_reserve`,
|
|
40
|
+
* `backup_reserve_switch`, `soc_calibration_enable`).
|
|
41
|
+
*/
|
|
42
|
+
export interface SolixSocParams {
|
|
43
|
+
chargeUpperLimit: number;
|
|
44
|
+
dischargeLowerLimit: number;
|
|
45
|
+
backupReserve: number;
|
|
46
|
+
backupReserveSwitch: number;
|
|
47
|
+
socCalibrationEnable: number;
|
|
48
|
+
}
|
|
49
|
+
/** One product in the pairable-product catalog. Extra vendor fields (images, guides) are preserved. */
|
|
50
|
+
export interface SolixProduct {
|
|
51
|
+
/** SKU / model code, e.g. `A1782`. */
|
|
52
|
+
product_code: string;
|
|
53
|
+
/** Marketing name, e.g. `SOLIX F3000`. */
|
|
54
|
+
name: string;
|
|
55
|
+
/** Variant/sub-model codes under this product, when present. */
|
|
56
|
+
p_codes?: unknown[];
|
|
57
|
+
[k: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
/** A catalog category (e.g. "Portable Power Station") and its products. */
|
|
60
|
+
export interface SolixProductCategory {
|
|
61
|
+
name: string;
|
|
62
|
+
products: SolixProduct[];
|
|
63
|
+
[k: string]: unknown;
|
|
64
|
+
}
|
|
65
|
+
/** One device's membership entry within a site, as carried by `get_site_list`'s `site_device_list`. */
|
|
66
|
+
export interface SolixSiteDeviceEntry {
|
|
67
|
+
device_sn: string;
|
|
68
|
+
/** The device's product/model code (the site list names this field `device_model`). */
|
|
69
|
+
device_model: string;
|
|
70
|
+
device_name?: string;
|
|
71
|
+
/** Anker device-type discriminator (e.g. 3 = Solarbank/battery, 6 = smart meter). */
|
|
72
|
+
device_type?: number;
|
|
73
|
+
[k: string]: unknown;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A site ("system") record, as returned by `SolixClient.getSites()`. A site is the account's home
|
|
77
|
+
* energy system — the "My Home" the app shows — grouping the member devices ({@link site_device_list})
|
|
78
|
+
* that a {@link SolixSiteReader} resolves into a `SolixSite`. Extra vendor fields are preserved.
|
|
79
|
+
*/
|
|
80
|
+
export interface SolixSiteRecord {
|
|
81
|
+
site_id: string;
|
|
82
|
+
site_name?: string;
|
|
83
|
+
/** Anker's site-type discriminator (e.g. 20 for a Solarbank-anchored home system). */
|
|
84
|
+
power_site_type?: number;
|
|
85
|
+
site_device_list?: SolixSiteDeviceEntry[];
|
|
86
|
+
[k: string]: unknown;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* One Solarbank/battery entry inside a site "scene" snapshot ({@link SolixSiteScene}). The scene mirrors
|
|
90
|
+
* the app's dashboard read: values arrive as STRINGS. Only the fields this SDK actually consumes are
|
|
91
|
+
* typed (chiefly `bat_temperature`, the realtime `ff09` push doesn't reliably carry); the index signature
|
|
92
|
+
* preserves the rest (`bat_charge_power`, `charging_status`, `load_port_*`, `function_switch`, …) verbatim.
|
|
93
|
+
*/
|
|
94
|
+
export interface SolixSceneSolarbank {
|
|
95
|
+
device_sn?: string;
|
|
96
|
+
device_pn?: string;
|
|
97
|
+
/** Battery pack temperature in °C (string on the wire). The scene is the reliable source for this. */
|
|
98
|
+
bat_temperature?: string | number;
|
|
99
|
+
/** State of charge, % (string on the wire). Cross-checks the `ff09` `0xa3` SOC. */
|
|
100
|
+
bat_soc?: string | number;
|
|
101
|
+
[k: string]: unknown;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* A site "scene" snapshot from {@link SolixClient.getSiteScene} — the app's dashboard read. The
|
|
105
|
+
* battery detail lives under `solarbank_info.solarbank_list` (NOT a top-level list). Only the shape this
|
|
106
|
+
* SDK reads is typed; everything else (grid_info, home_load_power, function flags, …) is preserved by the
|
|
107
|
+
* index signature. This is a low-rate backstop, never the realtime telemetry source.
|
|
108
|
+
*/
|
|
109
|
+
export interface SolixSiteScene {
|
|
110
|
+
solarbank_info?: {
|
|
111
|
+
solarbank_list?: SolixSceneSolarbank[];
|
|
112
|
+
[k: string]: unknown;
|
|
113
|
+
};
|
|
114
|
+
[k: string]: unknown;
|
|
115
|
+
}
|
package/dist/core/store.d.ts
CHANGED
|
@@ -5,6 +5,14 @@ import type { RegionShard } from "../transport/http/mega-client.js";
|
|
|
5
5
|
*/
|
|
6
6
|
export interface PersistedSession {
|
|
7
7
|
userId: string;
|
|
8
|
+
/**
|
|
9
|
+
* The eufy account's own `user_id` — the id the `gtoken` header is hashed from, which is not always the
|
|
10
|
+
* `ap_cloud_user_id` that `userId` prefers.
|
|
11
|
+
*
|
|
12
|
+
* Always written, equal to `userId` when the login reply carried only the one id. Absent therefore means a
|
|
13
|
+
* record from before this was tracked, which {@link isSessionValid} refuses rather than restore.
|
|
14
|
+
*/
|
|
15
|
+
accountUserId?: string;
|
|
8
16
|
authToken: string;
|
|
9
17
|
geoKey?: string;
|
|
10
18
|
region: RegionShard;
|
|
@@ -19,25 +27,45 @@ export interface PersistedSession {
|
|
|
19
27
|
tokenExpiresAt: number;
|
|
20
28
|
savedAt: number;
|
|
21
29
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
/**
|
|
31
|
+
* A place to persist a session record across runs. Parameterised on the record shape so other Anker
|
|
32
|
+
* lines (e.g. Solix, whose record is not a `PersistedSession`) can reuse the same file/memory
|
|
33
|
+
* stores rather than re-implementing them. Defaults to `PersistedSession` for the eufy path.
|
|
34
|
+
*/
|
|
35
|
+
export interface SessionStore<T = PersistedSession> {
|
|
36
|
+
load(): T | null;
|
|
37
|
+
save(s: T): void;
|
|
25
38
|
clear(): void;
|
|
26
39
|
}
|
|
27
40
|
/** In-memory store (no persistence) — the default. */
|
|
28
|
-
export declare class MemorySessionStore implements SessionStore {
|
|
41
|
+
export declare class MemorySessionStore<T = PersistedSession> implements SessionStore<T> {
|
|
29
42
|
private s;
|
|
30
|
-
load():
|
|
31
|
-
save(s:
|
|
43
|
+
load(): T | null;
|
|
44
|
+
save(s: T): void;
|
|
32
45
|
clear(): void;
|
|
33
46
|
}
|
|
34
47
|
/** JSON-file store, e.g. new FileSessionStore("./.eufy-session.json"). */
|
|
35
|
-
export declare class FileSessionStore implements SessionStore {
|
|
48
|
+
export declare class FileSessionStore<T = PersistedSession> implements SessionStore<T> {
|
|
36
49
|
private readonly path;
|
|
37
50
|
constructor(path: string);
|
|
38
|
-
load():
|
|
39
|
-
save(s:
|
|
51
|
+
load(): T | null;
|
|
52
|
+
save(s: T): void;
|
|
40
53
|
clear(): void;
|
|
41
54
|
}
|
|
42
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* A token is still usable if it has no known expiry, or expires more than `skewSec` from now. The one
|
|
57
|
+
* place the expiry/skew rule lives — reused by {@link isSessionValid} and by other lines' session checks
|
|
58
|
+
* (e.g. Solix) whose session shape differs but whose freshness rule is identical.
|
|
59
|
+
*/
|
|
60
|
+
export declare function tokenNotExpired(tokenExpiresAt: number | undefined, skewSec?: number): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* A persisted session is usable if it carries a complete credential — token, bound ECDH key, and the account
|
|
63
|
+
* id the `gtoken` header is hashed from — whose token isn't (near-)expired.
|
|
64
|
+
*
|
|
65
|
+
* `accountUserId` is part of the credential, not an optional extra: a record without it was written before
|
|
66
|
+
* that id was tracked, so it can only be restored as the other id, which is what the gateway rejects the
|
|
67
|
+
* header on. Nothing in a restored session can recover it either — the id arrives with a login reply. So such
|
|
68
|
+
* a record is refused and one login re-establishes it, rather than reinstating a session whose every
|
|
69
|
+
* authenticated call fails identically.
|
|
70
|
+
*/
|
|
43
71
|
export declare function isSessionValid(s: PersistedSession | null, skewSec?: number): boolean;
|