@mega-yfue/eufy-sdk 0.0.4 → 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -12
- package/dist/client/device-registry.d.ts +343 -0
- package/dist/client/eufy-mega.d.ts +872 -0
- package/dist/client/index.d.ts +1 -6
- package/dist/client/map-channels.d.ts +22 -0
- package/dist/client/types.d.ts +383 -0
- package/dist/core/contracts.d.ts +898 -0
- package/dist/core/crypto.d.ts +98 -0
- package/dist/core/index.d.ts +9 -7
- package/dist/core/logger.d.ts +53 -0
- package/dist/core/lz4-block.d.ts +35 -0
- package/dist/core/raw-dp-hex.d.ts +32 -0
- package/dist/core/raw-dp-writer.d.ts +83 -0
- package/dist/core/store.d.ts +43 -0
- package/dist/core/types.d.ts +169 -0
- package/dist/core/util.d.ts +78 -0
- package/dist/index.d.ts +4 -7
- package/dist/index.js +25526 -14
- package/dist/index.js.map +7 -1
- package/dist/model/capabilities/access.d.ts +127 -0
- package/dist/model/capabilities/arming.d.ts +201 -0
- package/dist/model/capabilities/audio.d.ts +154 -0
- package/dist/model/capabilities/battery.d.ts +363 -0
- package/dist/model/capabilities/camera.d.ts +564 -0
- package/dist/model/capabilities/co.d.ts +40 -0
- package/dist/model/capabilities/contact.d.ts +133 -0
- package/dist/model/capabilities/doorbell.d.ts +345 -0
- package/dist/model/capabilities/dp-catalog.d.ts +38 -0
- package/dist/model/capabilities/index.d.ts +561 -0
- package/dist/model/capabilities/info.d.ts +28 -0
- package/dist/model/capabilities/keypad.d.ts +61 -0
- package/dist/model/capabilities/leak.d.ts +43 -0
- package/dist/model/capabilities/light.d.ts +174 -0
- package/dist/model/capabilities/locate.d.ts +63 -0
- package/dist/model/capabilities/lock.d.ts +242 -0
- package/dist/model/capabilities/manifest.d.ts +107 -0
- package/dist/model/capabilities/members.d.ts +647 -0
- package/dist/model/capabilities/motion.d.ts +377 -0
- package/dist/model/capabilities/person-detection.d.ts +8 -0
- package/dist/model/capabilities/ptz.d.ts +289 -0
- package/dist/model/capabilities/rtsp.d.ts +221 -0
- package/dist/model/capabilities/siren.d.ts +218 -0
- package/dist/model/capabilities/smart-light.d.ts +172 -0
- package/dist/model/capabilities/smoke.d.ts +40 -0
- package/dist/model/capabilities/snapshot.d.ts +6 -0
- package/dist/model/capabilities/storage.d.ts +11 -0
- package/dist/model/capabilities/suction.d.ts +104 -0
- package/dist/model/capabilities/types.d.ts +484 -0
- package/dist/model/capabilities/vacuum-clean.d.ts +1946 -0
- package/dist/model/capabilities/vacuum-dock.d.ts +208 -0
- package/dist/model/capabilities/video.d.ts +6 -0
- package/dist/model/classify.d.ts +78 -0
- package/dist/model/clean-record-detail.d.ts +65 -0
- package/dist/model/clean-records.d.ts +69 -0
- package/dist/model/device-family.d.ts +73 -0
- package/dist/model/device-types.d.ts +123 -0
- package/dist/model/device.d.ts +265 -0
- package/dist/model/index.d.ts +29 -4
- package/dist/model/infer.d.ts +23 -0
- package/dist/model/inspect.d.ts +61 -0
- package/dist/model/life-params.d.ts +21 -0
- package/dist/model/map-pixels.d.ts +70 -0
- package/dist/model/param-dictionary.d.ts +28 -0
- package/dist/model/param-namespace.d.ts +21 -0
- package/dist/model/proto-read.d.ts +53 -0
- package/dist/model/push-events.d.ts +147 -0
- package/dist/model/registry.d.ts +54 -0
- package/dist/model/types.d.ts +301 -0
- package/dist/model/vacuum-map-store.d.ts +92 -0
- package/dist/model/vacuum-map.d.ts +286 -0
- package/dist/model/vacuum-scenes.d.ts +76 -0
- package/dist/model/vacuum-schedules.d.ts +85 -0
- package/dist/transport/dp-preset.d.ts +102 -0
- package/dist/transport/ff09.d.ts +444 -0
- package/dist/transport/ffmpeg.d.ts +86 -0
- package/dist/transport/http/decodeImageV1.d.ts +20 -0
- package/dist/transport/http/decodeImageV2.d.ts +19 -0
- package/dist/transport/http/index.d.ts +5 -0
- package/dist/transport/http/light-catalog.d.ts +62 -0
- package/dist/transport/http/media-download.d.ts +14 -0
- package/dist/transport/http/mega-client.d.ts +514 -0
- package/dist/transport/http/phone-model.d.ts +21 -0
- package/dist/transport/index.d.ts +10 -7
- package/dist/transport/mqtt/app-client-id.d.ts +16 -0
- package/dist/transport/mqtt/availability.d.ts +14 -0
- package/dist/transport/mqtt/bare-ip-tls.d.ts +46 -0
- package/dist/transport/mqtt/biz-stream.d.ts +98 -0
- package/dist/transport/mqtt/broker-discovery.d.ts +55 -0
- package/dist/transport/mqtt/clean-codec.d.ts +14 -0
- package/dist/transport/mqtt/command-router.d.ts +285 -0
- package/dist/transport/mqtt/dp-codec.d.ts +58 -0
- package/dist/transport/mqtt/dp-color.d.ts +14 -0
- package/dist/transport/mqtt/engine.d.ts +16 -0
- package/dist/transport/mqtt/index.d.ts +5 -0
- package/dist/transport/mqtt/secure-mqtt.d.ts +107 -0
- package/dist/transport/mqtt/topics.d.ts +80 -0
- package/dist/transport/p2p/adts.d.ts +91 -0
- package/dist/transport/p2p/annexb.d.ts +124 -0
- package/dist/transport/p2p/codec.d.ts +166 -0
- package/dist/transport/p2p/command-router.d.ts +655 -0
- package/dist/transport/p2p/commands.d.ts +550 -0
- package/dist/transport/p2p/envelope.d.ts +46 -0
- package/dist/transport/p2p/fmp4.d.ts +89 -0
- package/dist/transport/p2p/fragment-recording.d.ts +33 -0
- package/dist/transport/p2p/index.d.ts +13 -0
- package/dist/transport/p2p/lan-ip.d.ts +25 -0
- package/dist/transport/p2p/live-stream.d.ts +223 -0
- package/dist/transport/p2p/live-trace.d.ts +123 -0
- package/dist/transport/p2p/media.d.ts +105 -0
- package/dist/transport/p2p/p2p-session.d.ts +620 -0
- package/dist/transport/p2p/readable-egress.d.ts +27 -0
- package/dist/transport/p2p/session-manager.d.ts +154 -0
- package/dist/transport/p2p/shared-live-source.d.ts +431 -0
- package/dist/transport/p2p/talkback.d.ts +187 -0
- package/dist/transport/p2p/video.d.ts +150 -0
- package/dist/transport/p2p/write-commands.d.ts +21 -0
- package/dist/transport/protobuf.d.ts +5 -0
- package/dist/transport/push/fcm.d.ts +23 -0
- package/dist/transport/push/index.d.ts +6 -0
- package/dist/transport/push/message-tags.d.ts +26 -0
- package/dist/transport/push/parser.d.ts +27 -0
- package/dist/transport/push/proto.d.ts +11 -0
- package/dist/transport/push/push-client.d.ts +64 -0
- package/dist/transport/push/store.d.ts +23 -0
- package/dist/transport/push/types.d.ts +180 -0
- package/dist/transport/raw-dp.d.ts +6 -0
- package/dist/transport/stored-image-cache.d.ts +23 -0
- package/dist/transport/tuya/account.d.ts +44 -0
- package/dist/transport/tuya/client.d.ts +89 -0
- package/dist/transport/tuya/command-router.d.ts +79 -0
- package/dist/transport/tuya/dp-codec.d.ts +56 -0
- package/dist/transport/tuya/index.d.ts +28 -0
- package/dist/transport/tuya/request.d.ts +157 -0
- package/dist/transport/tuya/sign.d.ts +64 -0
- package/package.json +12 -13
- package/dist/client/index.js +0 -2
- package/dist/client/index.js.map +0 -1
- package/dist/core/index.js +0 -2
- package/dist/core/index.js.map +0 -1
- package/dist/model/index.js +0 -2
- package/dist/model/index.js.map +0 -1
- package/dist/transport/index.js +0 -2
- package/dist/transport/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
<img src="https://raw.githubusercontent.com/mega-yfue/eufy-sdk/main/docs/public/logo.svg" alt="eufy-sdk" height="72">
|
|
11
11
|
</picture>
|
|
12
12
|
|
|
13
|
-
**One typed client for the whole eufy ecosystem — devices, realtime events, and live media.**
|
|
13
|
+
**One typed client for the whole Anker eufy ecosystem — devices, realtime events, and live media.**
|
|
14
|
+
|
|
15
|
+
[](https://www.npmjs.com/package/@mega-yfue/eufy-sdk)
|
|
16
|
+
[](https://github.com/mega-yfue/eufy-sdk/actions/workflows/ci.yml)
|
|
17
|
+
[](./.nvmrc)
|
|
18
|
+
[](./LICENSE)
|
|
14
19
|
|
|
15
20
|
[Documentation](https://mega-yfue.github.io/) · [Contributing](./CONTRIBUTING.md) · [Changelog](./CHANGELOG.md)
|
|
16
21
|
|
|
@@ -25,14 +30,15 @@
|
|
|
25
30
|
|
|
26
31
|
## What it is
|
|
27
32
|
|
|
28
|
-
A TypeScript SDK for the eufy cloud that the current eufy app speaks. It logs in (captcha and 2FA
|
|
33
|
+
A TypeScript SDK for the Anker eufy cloud that the current eufy app speaks. It logs in (captcha and 2FA
|
|
29
34
|
included), keeps a **persistent session**, and models every device as a capability-driven `Device`
|
|
30
35
|
you drive through a **typed, fluent API**:
|
|
31
36
|
|
|
32
37
|
```ts
|
|
33
38
|
const dev = await eufy.getDevice(sn);
|
|
34
39
|
|
|
35
|
-
await dev.camera()?.
|
|
40
|
+
const stored = await dev.camera()?.snapshotStored?.(); // latest retained push JPEG
|
|
41
|
+
const fresh = await dev.camera()?.snapshotLive(); // explicit fresh live capture
|
|
36
42
|
await dev.panTilt()?.rotate(PtzDirection.left);
|
|
37
43
|
await dev.light()?.setBrightness(60);
|
|
38
44
|
|
|
@@ -49,14 +55,17 @@ code path and an unlisted or future device resolves the same way as a known one.
|
|
|
49
55
|
|
|
50
56
|
## Install
|
|
51
57
|
|
|
52
|
-
Once published:
|
|
53
|
-
|
|
54
58
|
```bash
|
|
55
59
|
npm install @mega-yfue/eufy-sdk
|
|
56
60
|
```
|
|
57
61
|
|
|
62
|
+
Releases go to **npmjs**, published from CI with provenance. Prereleases ship on the `beta` channel
|
|
63
|
+
(`npm install @mega-yfue/eufy-sdk@beta`) while a version is still under review.
|
|
64
|
+
|
|
58
65
|
**Node.js ≥ 24.5.0** is required, not just recommended (see [`.nvmrc`](./.nvmrc)). `ffmpeg` is
|
|
59
|
-
optional — only the JPEG
|
|
66
|
+
optional — only the live JPEG snapshot and one-shot mp4 record paths use it,
|
|
67
|
+
and a host that ships its own build names it with `new EufyMega({ ffmpegPath })` rather than needing
|
|
68
|
+
one on `PATH`.
|
|
60
69
|
|
|
61
70
|
## Documentation
|
|
62
71
|
|
|
@@ -72,7 +81,7 @@ Four layers, one dependency direction — `core` → `transport` → `model` →
|
|
|
72
81
|
```
|
|
73
82
|
src/
|
|
74
83
|
core/ shared floor: crypto, cross-layer contracts, value types, session store
|
|
75
|
-
transport/ every byte-on-a-wire module: http, mqtt, p2p, push,
|
|
84
|
+
transport/ every byte-on-a-wire module: http, mqtt, p2p, push, tuya
|
|
76
85
|
model/ Device + one self-contained module per capability
|
|
77
86
|
client/ the facade: login, device registry, event fan-out
|
|
78
87
|
index.ts public surface — one `export *` per layer barrel
|
|
@@ -83,7 +92,7 @@ src/
|
|
|
83
92
|
never names a feature. Anything genuinely shared is a contract in `core/`. That rule and the rest of
|
|
84
93
|
the code practice are in [AGENTS.md](./AGENTS.md).
|
|
85
94
|
|
|
86
|
-
Three runtime dependencies — `mqtt`, `protobufjs`, `
|
|
95
|
+
Three runtime dependencies — `mqtt`, `protobufjs`, `jpeg-js` — and that is deliberate. HTTP is native
|
|
87
96
|
`fetch`, hashing and ciphers are `node:crypto`, 64-bit integers are `BigInt`.
|
|
88
97
|
|
|
89
98
|
**Unverified write paths throw rather than guess.** Some writes are fire-and-forget, so a guessed
|
|
@@ -103,13 +112,23 @@ PRs welcome. [CONTRIBUTING.md](./CONTRIBUTING.md) covers setup, the dev workflow
|
|
|
103
112
|
[AGENTS.md](./AGENTS.md) is the code practice — the architecture invariants and the rules CI enforces.
|
|
104
113
|
Security issues go through [SECURITY.md](./SECURITY.md), never a public issue.
|
|
105
114
|
|
|
115
|
+
## Thanks
|
|
116
|
+
|
|
117
|
+
Huge thanks to the testers who run this against real hardware and make sure it's ready — they are
|
|
118
|
+
credited in the release notes for the version their work landed in.
|
|
119
|
+
|
|
106
120
|
## License
|
|
107
121
|
|
|
108
122
|
[Apache-2.0](./LICENSE). Contributions are accepted under the same license (inbound = outbound).
|
|
109
123
|
|
|
110
124
|
## Disclaimer
|
|
111
125
|
|
|
112
|
-
Independent and unofficial, built for interoperability with eufy devices you own. **Not
|
|
113
|
-
with, endorsed by, or sponsored by Anker Innovations or eufy.** "eufy"
|
|
114
|
-
their respective owners and appear here only to identify the
|
|
115
|
-
responsibly — rapid or failed logins can trigger a captcha or a
|
|
126
|
+
Independent and unofficial, built for interoperability with Anker eufy devices you own. **Not
|
|
127
|
+
affiliated with, endorsed by, or sponsored by Anker Innovations, Anker eufy, or eufy.** "Anker eufy",
|
|
128
|
+
"eufy" and "Anker" are trademarks of their respective owners and appear here only to identify the
|
|
129
|
+
hardware this SDK talks to. Use responsibly — rapid or failed logins can trigger a captcha or a
|
|
130
|
+
temporary cooldown.
|
|
131
|
+
|
|
132
|
+
The vendor now brands the line **Anker eufy**; "eufy" alone is the short form and still the name on
|
|
133
|
+
the wire (`eufy_security`, `eufy_life`, `eufy_mega`) and in every product name (eufyCam, eufy Clean,
|
|
134
|
+
eufy Life). Protocol vocabulary follows the device, not the marketing, so those are not renamed here.
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The station a device's traffic belongs to, from its cloud record and its own serial.
|
|
3
|
+
*
|
|
4
|
+
* `parent_sn` carries the parent on a HomeBase-attached device. `station_sn` is frequently absent there —
|
|
5
|
+
* empty on every attached sensor of a T8010 — and serves only as a fallback. An empty string states no
|
|
6
|
+
* station.
|
|
7
|
+
*
|
|
8
|
+
* A device naming no parent answers its own serial, so every device has a station.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolvedStationSn(raw: Record<string, unknown>, sn: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* DeviceRegistry — the device list/record/capability-resolution collaborator behind {@link EufyMega}.
|
|
13
|
+
*
|
|
14
|
+
* The facade owns orchestration + event fan-out; this owns the resolution logic: fetching + merging
|
|
15
|
+
* the (house-scoped, quirky) device list, overlaying a fresh param list per device, and resolving which
|
|
16
|
+
* capability set a P2P frame's (station, channel) belongs to (a hot-path cache). Isolated here so the
|
|
17
|
+
* tricky bits are unit-testable with a fake `mega` — the facade stays pure wiring. It never names a
|
|
18
|
+
* capability or a wire; it maps records to the model's `resolveDevice`/`inspectParams`.
|
|
19
|
+
*/
|
|
20
|
+
import { MegaHttpClient } from "../transport/http/mega-client.js";
|
|
21
|
+
import { type Logger } from "../core/logger.js";
|
|
22
|
+
import { type EufyDevice } from "../core/types.js";
|
|
23
|
+
import { type Capability, type DeviceInspection } from "../model/index.js";
|
|
24
|
+
/** The device-record shape {@link EufyMega.getDevice}/`commandContext` resolve a serial to. */
|
|
25
|
+
export interface DeviceRecord {
|
|
26
|
+
deviceType?: number;
|
|
27
|
+
model?: string;
|
|
28
|
+
category?: string;
|
|
29
|
+
/** The app-shown device name (`device_name`); see {@link CloudRecord.name}. */
|
|
30
|
+
name?: string;
|
|
31
|
+
/** Parent HomeBase serial when attached (topology signal; see {@link CloudRecord.parentSn}). */
|
|
32
|
+
parentSn?: string;
|
|
33
|
+
params: Record<number, string>;
|
|
34
|
+
/** Per-param `update_time` in **unix seconds** (see {@link EufyDevice.paramUpdatedAt}). */
|
|
35
|
+
paramUpdatedAt: Record<number, number>;
|
|
36
|
+
/**
|
|
37
|
+
* State the device reported over its realtime wire, as ids in its own param namespace.
|
|
38
|
+
*
|
|
39
|
+
* Kept apart from {@link params} rather than merged into it because the two differ in provenance and
|
|
40
|
+
* staleness: cloud params are a slow server-side heartbeat snapshot, these are the device's own live
|
|
41
|
+
* report. Capability detection reads only `params`, so a realtime id can never steer which
|
|
42
|
+
* capabilities a device is judged to have.
|
|
43
|
+
*/
|
|
44
|
+
dpParams?: Record<number, string>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* One param that changed value between two polls — the input the facade turns into a
|
|
48
|
+
* `source:"poll"` inbound signal. Carries the whole post-change param map so a capability that needs
|
|
49
|
+
* sibling params to interpret the change can read them.
|
|
50
|
+
*/
|
|
51
|
+
export interface ParamChange {
|
|
52
|
+
deviceSn: string;
|
|
53
|
+
paramType: number;
|
|
54
|
+
from: string;
|
|
55
|
+
to: string;
|
|
56
|
+
params: Record<number, string>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* What one poll pass observed: params whose value moved, devices that joined or left the account, and
|
|
60
|
+
* devices that merely re-reported. All four come from a single device-list fetch.
|
|
61
|
+
*/
|
|
62
|
+
export interface PollDiff {
|
|
63
|
+
params: ParamChange[];
|
|
64
|
+
added: EufyDevice[];
|
|
65
|
+
removed: EufyDevice[];
|
|
66
|
+
/**
|
|
67
|
+
* Devices whose {@link EufyDevice.lastSeenMs} advanced since the previous pass — fresh proof the
|
|
68
|
+
* device is alive, tracked apart from {@link params} because the cloud can re-stamp a param with an
|
|
69
|
+
* unchanged VALUE. That is no state change to report, but it is a liveness signal. A device seen for
|
|
70
|
+
* the first time is absent here: first sight is discovery, not a transition.
|
|
71
|
+
*/
|
|
72
|
+
reported: EufyDevice[];
|
|
73
|
+
}
|
|
74
|
+
export interface DeviceRegistryDeps {
|
|
75
|
+
mega: MegaHttpClient;
|
|
76
|
+
/** Surface a non-fatal fetch error (a house/body query that failed) without aborting the merge. */
|
|
77
|
+
onError: (e: unknown) => void;
|
|
78
|
+
/** Diagnostics sink for facts that are NOT errors — see the owner-gated overlay note on {@link DeviceRegistry.record}. */
|
|
79
|
+
logger?: Logger;
|
|
80
|
+
}
|
|
81
|
+
export declare class DeviceRegistry {
|
|
82
|
+
private readonly mega;
|
|
83
|
+
private readonly onError;
|
|
84
|
+
private readonly logger;
|
|
85
|
+
private devices;
|
|
86
|
+
/** Per-(station, channel) capability cache for {@link capabilitiesForFrame}; `null` = negative hit. */
|
|
87
|
+
private readonly frameCapsCache;
|
|
88
|
+
/**
|
|
89
|
+
* Serials whose per-device param overlay has been refused. The call is owner-gated, so on a shared or
|
|
90
|
+
* member account it fails for the whole life of the client — retrying it every refresh spends a request
|
|
91
|
+
* to learn the same thing, and the answer is reported once rather than on every read.
|
|
92
|
+
*/
|
|
93
|
+
private readonly overlayRefused;
|
|
94
|
+
/** One in-flight device-list fetch shared by every caller that wants a fresher list. See {@link refreshedList}. */
|
|
95
|
+
private listInFlight?;
|
|
96
|
+
/** When the device list was last fetched, so a burst of per-device reads shares one. See {@link refreshedList}. */
|
|
97
|
+
private listFetchedAtMs;
|
|
98
|
+
/** Whether the owner-gated refusal has been reported. It is one fact about the account, so it is said once. */
|
|
99
|
+
private overlayRefusalReported;
|
|
100
|
+
/** Per-serial capability cache for {@link capabilitiesForDevice}; `null` = negative hit. */
|
|
101
|
+
private readonly deviceCapsCache;
|
|
102
|
+
/** Per-serial realtime state, keyed by param id in the device's own namespace (see {@link DeviceRecord.dpParams}). */
|
|
103
|
+
private readonly dpParams;
|
|
104
|
+
/** Callers blocked in {@link awaitRealtimeState}, released by the device's first report. */
|
|
105
|
+
private readonly stateWaiters;
|
|
106
|
+
/**
|
|
107
|
+
* Whether the last {@link getDevices} lost at least one query. The house-scoped fetch tolerates a
|
|
108
|
+
* failing house/body call so a partial outage still yields devices — but the result is then a
|
|
109
|
+
* SUBSET, so a missing device is not a removal.
|
|
110
|
+
*/
|
|
111
|
+
private lastRefreshPartial;
|
|
112
|
+
/**
|
|
113
|
+
* The roster {@link pollChanges} last diffed against, and whether it was complete.
|
|
114
|
+
*
|
|
115
|
+
* Held separately from {@link devices} because that cache is refreshed by anything that needs a
|
|
116
|
+
* device — a host's own `getDevices()`, a command sink resolving a serial before an on-demand P2P
|
|
117
|
+
* open. Diffing the shared cache in place would let any of those silently absorb the delta, and the
|
|
118
|
+
* next poll would then see an unchanged account and emit nothing. `undefined` = never polled.
|
|
119
|
+
*
|
|
120
|
+
* It holds the VALUES the diff reads, never the records themselves. Sharing the records lets anything that
|
|
121
|
+
* updates one in place rewrite the baseline before the next pass can diff against it — and the realtime
|
|
122
|
+
* path does exactly that to `lastSeenMs`: a station's report stamps the record the baseline is holding, so
|
|
123
|
+
* the next pass compares the cloud's older timestamp against a baseline already advanced to now and
|
|
124
|
+
* reports nothing. Every device that reports over realtime loses its poll liveness signal that way.
|
|
125
|
+
*
|
|
126
|
+
* The params are copied for the same reason, against a mutation no current path performs: nothing writes
|
|
127
|
+
* into a record's param map in place today (a realtime report lands in its own map, and a refetch rebuilds
|
|
128
|
+
* the record), so that half is a latent hazard rather than an observed one — copied because the diff
|
|
129
|
+
* cannot tell the difference and the cost is one shallow copy per device per pass.
|
|
130
|
+
*/
|
|
131
|
+
private pollSnapshot?;
|
|
132
|
+
constructor(deps: DeviceRegistryDeps);
|
|
133
|
+
/** The current device cache (last {@link getDevices} result). */
|
|
134
|
+
list(): EufyDevice[];
|
|
135
|
+
/**
|
|
136
|
+
* List + classify devices across all houses (mega API). Each device is tagged
|
|
137
|
+
* with its API backend + realtime transport (see classifyDevice). Camera/
|
|
138
|
+
* HomeBase records still appear here for inventory; driving them is P2P.
|
|
139
|
+
*
|
|
140
|
+
* A failing house/body query is tolerated so a partial outage still yields devices — but the result
|
|
141
|
+
* is then a SUBSET, and devices it didn't return are KEPT from the previous list rather than dropped.
|
|
142
|
+
* Replacing wholesale would empty the cache during an outage, breaking every serial lookup the
|
|
143
|
+
* command sink and event fan-out depend on, and would then present the whole account as newly
|
|
144
|
+
* discovered once the next refresh succeeded. {@link lastRefreshPartial} records that this happened.
|
|
145
|
+
*
|
|
146
|
+
* A **rejected session** is the one failure not tolerated that way, because it is not a subset of
|
|
147
|
+
* anything: every query fails identically, so what is left to return is nothing on a fresh client — an
|
|
148
|
+
* empty account that reads exactly like an account with no devices. The transport has already tried to
|
|
149
|
+
* replace the token by logging in again, so reaching here means it could not, and the caller is the one
|
|
150
|
+
* who has to know. It rejects; the devices it already knew stay known.
|
|
151
|
+
*/
|
|
152
|
+
getDevices(): Promise<EufyDevice[]>;
|
|
153
|
+
/**
|
|
154
|
+
* Re-fetch the device list once and report what changed: params whose VALUE moved, plus devices that
|
|
155
|
+
* joined or left the account.
|
|
156
|
+
*
|
|
157
|
+
* Diffs the bulk list rather than polling devices one by one — `getDevices()` already refreshes the
|
|
158
|
+
* whole account in one house-scoped pass, so per-device fetching would multiply requests for the same
|
|
159
|
+
* data, and roster changes fall out of the same response for free.
|
|
160
|
+
*
|
|
161
|
+
* A param is a change only if it existed before with a different value. Newly-appeared params and
|
|
162
|
+
* every param of a newly-appeared device are NOT changes: a device showing up for the first time is
|
|
163
|
+
* discovery (reported as `added`), and treating its whole param set as changes would fire a burst of
|
|
164
|
+
* phantom state events on the first poll.
|
|
165
|
+
*
|
|
166
|
+
* Both halves of the roster diff are gated on the baseline being trustworthy, in opposite directions.
|
|
167
|
+
* `removed` is suppressed when THIS refresh was {@link lastRefreshPartial} — a failed house query
|
|
168
|
+
* yields a subset of the account, and reporting those absences as removals would report a device that
|
|
169
|
+
* is simply unqueried as gone. `added` is suppressed when the PREVIOUS snapshot was
|
|
170
|
+
* incomplete, for the mirror-image reason: a device missing from a partial baseline is not new, and
|
|
171
|
+
* announcing it would present a chunk of an existing account as freshly discovered.
|
|
172
|
+
*
|
|
173
|
+
* The very first pass reports no additions at all — the whole account is the baseline, not a
|
|
174
|
+
* pairing burst. {@link getDevices} enumerates what exists.
|
|
175
|
+
*/
|
|
176
|
+
pollChanges(): Promise<PollDiff>;
|
|
177
|
+
/** Devices driven over eufy secure-MQTT — named positively (not "everything that isn't P2P"), so a
|
|
178
|
+
* device on another realtime plane (e.g. a printer's `ankermake-mqtt`) is not swept onto this one. */
|
|
179
|
+
mqttDevices(): EufyDevice[];
|
|
180
|
+
/** Devices that require P2P (cameras/HomeBases). */
|
|
181
|
+
p2pDevices(): EufyDevice[];
|
|
182
|
+
/**
|
|
183
|
+
* Resolve a serial to its cached {@link EufyDevice}, **throwing if it isn't loaded** — the loud
|
|
184
|
+
* synchronous lookup the facade's command sink uses before asking a transport stack whether it claims
|
|
185
|
+
* the device (`P2PCommandRouter.claimsDevice` / `MqttCommandRouter.claimsDevice`). A routing decision
|
|
186
|
+
* must never fall back on a missing record (an unknown serial silently routing to the wrong transport
|
|
187
|
+
* would misroute a fire-and-forget command with no error). Registry owns record resolution; the
|
|
188
|
+
* transport stacks own the claim predicate over the record.
|
|
189
|
+
*/
|
|
190
|
+
require(sn: string): EufyDevice;
|
|
191
|
+
/**
|
|
192
|
+
* Resolve a serial to a {@link DeviceRecord} with current params: starts from the device-list params, then
|
|
193
|
+
* overlays a fresh `get_device_param_list` when that call is available to this account.
|
|
194
|
+
*
|
|
195
|
+
* The overlay is **owner-gated** — a shared or member account is refused it for every device, permanently,
|
|
196
|
+
* which {@link OWNER_ONLY_CODE} identifies — so when it is unavailable the device list is the source
|
|
197
|
+
* instead. Any OTHER failure is treated as transient: it falls back for that call but is retried next
|
|
198
|
+
* time, because latching on a timeout would cost an entitled account its freshest source of params.
|
|
199
|
+
*
|
|
200
|
+
* A dead session is the exception: it is not a statement about the overlay's availability, and the fallback
|
|
201
|
+
* runs over the same session, so it propagates rather than degrading to the params this call already held.
|
|
202
|
+
* Serving those as current would report an expired token as a device that simply has not changed.
|
|
203
|
+
*
|
|
204
|
+
* The refusal is **logged, never surfaced as an error**. It is a normal property of a shared or member
|
|
205
|
+
* account, not a fault: nothing failed that the SDK did not immediately handle, and the account holder
|
|
206
|
+
* cannot grant themselves ownership.
|
|
207
|
+
*
|
|
208
|
+
* The list is account-wide, so a re-fetch is NOT per device: resolving a fleet calls this once per device,
|
|
209
|
+
* and each one re-fetching would multiply one burst into N. {@link refreshedList} reuses a list younger
|
|
210
|
+
* than {@link LIST_REUSE_MS} and coalesces concurrent fetches, which keeps resolving N devices at the cost
|
|
211
|
+
* of one list while still letting a later refresh see a new value. That list is not owner-gated and carries the same
|
|
212
|
+
* `{param_type, param_value, update_time}`, which makes it the fallback the overlay's own contract names.
|
|
213
|
+
*
|
|
214
|
+
* Shared by {@link EufyMega.getDevice} / {@link EufyMega.inspectDevice} / `commandContext`.
|
|
215
|
+
*/
|
|
216
|
+
record(sn: string): Promise<DeviceRecord>;
|
|
217
|
+
/**
|
|
218
|
+
* Re-fetch the device list, coalescing concurrent callers onto one in-flight fetch and reusing one
|
|
219
|
+
* younger than {@link LIST_REUSE_MS}. Answers with this serial's record from that list, or `undefined`.
|
|
220
|
+
*
|
|
221
|
+
* The list is account-wide (`get_house_list` plus one `get_devs_list` per house), so without this a refresh
|
|
222
|
+
* cycle over N devices would multiply into N of those bursts — and every fetch clears the capability caches,
|
|
223
|
+
* so they would stop working. One fetch serves every device that wants the same answer.
|
|
224
|
+
*
|
|
225
|
+
* This is the ONLY way a caller in a loop should ask for a fresher list. A convergence wait that polls
|
|
226
|
+
* {@link getDevices} directly bypasses both the window and the coalescing, so one write whose param never
|
|
227
|
+
* lands spends a whole account-wide burst per iteration, and concurrent transitions multiply that again.
|
|
228
|
+
*
|
|
229
|
+
* Only a fetch that RESOLVED opens the reuse window. {@link getDevices} tolerates a failing house/body
|
|
230
|
+
* query as a partial and answers anyway, so an outage still holds the window on purpose — retrying per
|
|
231
|
+
* device is how one outage becomes N bursts. What must not hold it is the one failure that rejects: a dead
|
|
232
|
+
* session, which also propagates rather than degrading to `undefined`, because answering "no such device"
|
|
233
|
+
* for an expired token is the same lie {@link getDevices} stopped telling, one level down.
|
|
234
|
+
*/
|
|
235
|
+
refreshedList(sn: string): Promise<EufyDevice | undefined>;
|
|
236
|
+
/**
|
|
237
|
+
* Record state a device reported over its realtime wire, merging into whatever it last reported.
|
|
238
|
+
*
|
|
239
|
+
* Merged rather than replaced because a report can be partial — a status frame that omits a field is
|
|
240
|
+
* silent about it, not asserting it went away. Marks the device seen and drops its capability cache,
|
|
241
|
+
* since a newly-reported id can widen the evidence-gated read surface.
|
|
242
|
+
*
|
|
243
|
+
* What lands here outranks the cloud half in {@link record}, and stays there until
|
|
244
|
+
* {@link retireRealtimeParams} says the cloud has moved that id itself.
|
|
245
|
+
*/
|
|
246
|
+
applyRealtimeParams(sn: string, params: Record<number, string>): void;
|
|
247
|
+
/**
|
|
248
|
+
* Drop this device's reported value for these param ids, because the CLOUD has since been observed to
|
|
249
|
+
* move them — {@link EufyMega} calls this with the ids of a poll diff.
|
|
250
|
+
*
|
|
251
|
+
* {@link record} joins the two halves by letting the report win, which is right only while the report
|
|
252
|
+
* is the fresher of the two: the cloud list carries a pre-report value long after the device
|
|
253
|
+
* volunteered the new one, so without that precedence an open door reads as closed. A poll diff on the
|
|
254
|
+
* same id is the cloud stating a transition it observed, which ends the lag the report was standing in
|
|
255
|
+
* for. Leaving the report in place would make it outrank the cloud permanently, and every later join
|
|
256
|
+
* would revert that id to a value the cloud has already superseded.
|
|
257
|
+
*
|
|
258
|
+
* Ids alone, never a value: this says the report is out of date, not what replaced it. The replacement
|
|
259
|
+
* is already in the cloud half, and writing it in here would put one value in two maps for the next
|
|
260
|
+
* change to disagree about.
|
|
261
|
+
*
|
|
262
|
+
* The capability cache is deliberately NOT dropped: an id stops being remembered here, but the device
|
|
263
|
+
* did report it, and evidence-gated reads are granted on having reported — retracting that would take
|
|
264
|
+
* a getter away from a `Device` that legitimately earned it. The map itself stays for the same reason
|
|
265
|
+
* even once emptied, since its presence is what {@link hasRealtimeState} answers "this device has
|
|
266
|
+
* reported" from, and a device does not become one that never reported.
|
|
267
|
+
*/
|
|
268
|
+
retireRealtimeParams(sn: string, paramTypes: readonly number[]): void;
|
|
269
|
+
/** Whether this device has reported any realtime state yet. */
|
|
270
|
+
hasRealtimeState(sn: string): boolean;
|
|
271
|
+
/**
|
|
272
|
+
* Resolve once this device reports realtime state, or after `timeoutMs` — whichever comes first.
|
|
273
|
+
*
|
|
274
|
+
* Exists because the typed read getters are evidence-gated at BIND time: a device resolved before its
|
|
275
|
+
* first report gets no getters, and would keep none however much state arrived afterwards. Resolving
|
|
276
|
+
* (not rejecting) on timeout keeps a silent device merely read-less rather than unusable.
|
|
277
|
+
*/
|
|
278
|
+
awaitRealtimeState(sn: string, timeoutMs: number): Promise<void>;
|
|
279
|
+
/**
|
|
280
|
+
* Inspect one device by serial: resolve its codec/capabilities, cross-reference every reported
|
|
281
|
+
* `param_type` against the param dictionary, and emit a paste-ready `registry.ts` row plus
|
|
282
|
+
* dictionary snippets for anything unknown.
|
|
283
|
+
*/
|
|
284
|
+
inspectDevice(sn: string): Promise<DeviceInspection>;
|
|
285
|
+
/**
|
|
286
|
+
* The serial of the device a P2P frame belongs to, resolved by the same `(station, channel)` pair
|
|
287
|
+
* as {@link capabilitiesForFrame} — the identity half of the same question.
|
|
288
|
+
*
|
|
289
|
+
* A frame-sourced semantic event carries the STATION only, which cannot say which attached device
|
|
290
|
+
* reported it: a station fans several same-kind sensors out by channel, so two entry sensors on one
|
|
291
|
+
* hub would emit indistinguishable events. The facade enriches the payload with this.
|
|
292
|
+
*/
|
|
293
|
+
serialForFrame(stationSn: string, channel: number): string | undefined;
|
|
294
|
+
/**
|
|
295
|
+
* The parent station a device's frames arrive under.
|
|
296
|
+
*
|
|
297
|
+
* `parent_sn` on the cloud record is the field that is actually populated for a HomeBase-attached
|
|
298
|
+
* device — `stationSn` is frequently absent (observed empty on every attached sensor of a T8010),
|
|
299
|
+
* so keying on it alone silently resolves an attached device to ITSELF and no frame ever matches.
|
|
300
|
+
* Mirrors the router's own session-keying precedence, which is the source of truth for which
|
|
301
|
+
* station a device's traffic belongs to. Answering the device's OWN serial is what "stands alone"
|
|
302
|
+
* means, so this is also the topology signal `record()`/`capsOf` hand the resolver.
|
|
303
|
+
*/
|
|
304
|
+
private stationOf;
|
|
305
|
+
/**
|
|
306
|
+
* The device a `(station, channel)` pair refers to — a station fans out to attached devices by
|
|
307
|
+
* `device_channel`, while a standalone device is its own station at channel 0.
|
|
308
|
+
*
|
|
309
|
+
* A device claims a channel only when its record actually STATES one. Treating a missing
|
|
310
|
+
* `device_channel` as 0 turns every such device into a rival claimant for channel 0, where a
|
|
311
|
+
* station legitimately has an attached device already, and the winner is then decided by cloud list
|
|
312
|
+
* order — so the same frame resolves to different devices across refreshes. The resolved serial now
|
|
313
|
+
* decides where realtime state is written, not just which decoders may run, so an ambiguous answer
|
|
314
|
+
* writes one device's params onto another.
|
|
315
|
+
*
|
|
316
|
+
* An attached device that names the channel wins over the station itself, which is what a station
|
|
317
|
+
* fanning traffic out by channel means; the station answers for channel 0 only when nothing is
|
|
318
|
+
* attached there, which is also the standalone case (a device is its own station).
|
|
319
|
+
*/
|
|
320
|
+
private deviceForFrame;
|
|
321
|
+
/**
|
|
322
|
+
* Resolve the capability set of the device a P2P frame belongs to — the `(station, channel)` pair
|
|
323
|
+
* (a station fans out to attached devices by `device_channel`; a standalone device is its own
|
|
324
|
+
* station at channel 0). Used to gate the p2p-frame escape-hatch decoders.
|
|
325
|
+
*
|
|
326
|
+
* Runs on the P2P data hot path, so it is synchronous over the already-cached device list (no
|
|
327
|
+
* await / network) and memoized per (station, channel). Returns `undefined` when the device can't
|
|
328
|
+
* be resolved yet — the caller then falls back to running every module.
|
|
329
|
+
*/
|
|
330
|
+
capabilitiesForFrame(stationSn: string, channel: number): ReadonlySet<Capability> | undefined;
|
|
331
|
+
/**
|
|
332
|
+
* The capability set of a device by serial — the disambiguator for **push / poll** events, whose ids
|
|
333
|
+
* are namespaced per device family and therefore collide across families (a SmartDrop's tamper id is
|
|
334
|
+
* a HomeBase's alarm id). Same role {@link capabilitiesForFrame} plays for P2P frames, keyed by
|
|
335
|
+
* serial because a push carries `deviceSn` rather than a (station, channel).
|
|
336
|
+
*
|
|
337
|
+
* Synchronous over the cached device list and memoized; `undefined` when the serial isn't loaded, so
|
|
338
|
+
* a caller can tell "device has no such capability" from "device unknown" and refuse to guess.
|
|
339
|
+
*/
|
|
340
|
+
capabilitiesForDevice(sn: string): ReadonlySet<Capability> | undefined;
|
|
341
|
+
/** Resolve a record's capabilities the way `getDevice` does, so gating matches `device.has()`. */
|
|
342
|
+
private capsOf;
|
|
343
|
+
}
|