@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.
Files changed (143) hide show
  1. package/README.md +31 -12
  2. package/dist/client/device-registry.d.ts +343 -0
  3. package/dist/client/eufy-mega.d.ts +872 -0
  4. package/dist/client/index.d.ts +1 -6
  5. package/dist/client/map-channels.d.ts +22 -0
  6. package/dist/client/types.d.ts +383 -0
  7. package/dist/core/contracts.d.ts +898 -0
  8. package/dist/core/crypto.d.ts +98 -0
  9. package/dist/core/index.d.ts +9 -7
  10. package/dist/core/logger.d.ts +53 -0
  11. package/dist/core/lz4-block.d.ts +35 -0
  12. package/dist/core/raw-dp-hex.d.ts +32 -0
  13. package/dist/core/raw-dp-writer.d.ts +83 -0
  14. package/dist/core/store.d.ts +43 -0
  15. package/dist/core/types.d.ts +169 -0
  16. package/dist/core/util.d.ts +78 -0
  17. package/dist/index.d.ts +4 -7
  18. package/dist/index.js +25526 -14
  19. package/dist/index.js.map +7 -1
  20. package/dist/model/capabilities/access.d.ts +127 -0
  21. package/dist/model/capabilities/arming.d.ts +201 -0
  22. package/dist/model/capabilities/audio.d.ts +154 -0
  23. package/dist/model/capabilities/battery.d.ts +363 -0
  24. package/dist/model/capabilities/camera.d.ts +564 -0
  25. package/dist/model/capabilities/co.d.ts +40 -0
  26. package/dist/model/capabilities/contact.d.ts +133 -0
  27. package/dist/model/capabilities/doorbell.d.ts +345 -0
  28. package/dist/model/capabilities/dp-catalog.d.ts +38 -0
  29. package/dist/model/capabilities/index.d.ts +561 -0
  30. package/dist/model/capabilities/info.d.ts +28 -0
  31. package/dist/model/capabilities/keypad.d.ts +61 -0
  32. package/dist/model/capabilities/leak.d.ts +43 -0
  33. package/dist/model/capabilities/light.d.ts +174 -0
  34. package/dist/model/capabilities/locate.d.ts +63 -0
  35. package/dist/model/capabilities/lock.d.ts +242 -0
  36. package/dist/model/capabilities/manifest.d.ts +107 -0
  37. package/dist/model/capabilities/members.d.ts +647 -0
  38. package/dist/model/capabilities/motion.d.ts +377 -0
  39. package/dist/model/capabilities/person-detection.d.ts +8 -0
  40. package/dist/model/capabilities/ptz.d.ts +289 -0
  41. package/dist/model/capabilities/rtsp.d.ts +221 -0
  42. package/dist/model/capabilities/siren.d.ts +218 -0
  43. package/dist/model/capabilities/smart-light.d.ts +172 -0
  44. package/dist/model/capabilities/smoke.d.ts +40 -0
  45. package/dist/model/capabilities/snapshot.d.ts +6 -0
  46. package/dist/model/capabilities/storage.d.ts +11 -0
  47. package/dist/model/capabilities/suction.d.ts +104 -0
  48. package/dist/model/capabilities/types.d.ts +484 -0
  49. package/dist/model/capabilities/vacuum-clean.d.ts +1946 -0
  50. package/dist/model/capabilities/vacuum-dock.d.ts +208 -0
  51. package/dist/model/capabilities/video.d.ts +6 -0
  52. package/dist/model/classify.d.ts +78 -0
  53. package/dist/model/clean-record-detail.d.ts +65 -0
  54. package/dist/model/clean-records.d.ts +69 -0
  55. package/dist/model/device-family.d.ts +73 -0
  56. package/dist/model/device-types.d.ts +123 -0
  57. package/dist/model/device.d.ts +265 -0
  58. package/dist/model/index.d.ts +29 -4
  59. package/dist/model/infer.d.ts +23 -0
  60. package/dist/model/inspect.d.ts +61 -0
  61. package/dist/model/life-params.d.ts +21 -0
  62. package/dist/model/map-pixels.d.ts +70 -0
  63. package/dist/model/param-dictionary.d.ts +28 -0
  64. package/dist/model/param-namespace.d.ts +21 -0
  65. package/dist/model/proto-read.d.ts +53 -0
  66. package/dist/model/push-events.d.ts +147 -0
  67. package/dist/model/registry.d.ts +54 -0
  68. package/dist/model/types.d.ts +301 -0
  69. package/dist/model/vacuum-map-store.d.ts +92 -0
  70. package/dist/model/vacuum-map.d.ts +286 -0
  71. package/dist/model/vacuum-scenes.d.ts +76 -0
  72. package/dist/model/vacuum-schedules.d.ts +85 -0
  73. package/dist/transport/dp-preset.d.ts +102 -0
  74. package/dist/transport/ff09.d.ts +444 -0
  75. package/dist/transport/ffmpeg.d.ts +86 -0
  76. package/dist/transport/http/decodeImageV1.d.ts +20 -0
  77. package/dist/transport/http/decodeImageV2.d.ts +19 -0
  78. package/dist/transport/http/index.d.ts +5 -0
  79. package/dist/transport/http/light-catalog.d.ts +62 -0
  80. package/dist/transport/http/media-download.d.ts +14 -0
  81. package/dist/transport/http/mega-client.d.ts +514 -0
  82. package/dist/transport/http/phone-model.d.ts +21 -0
  83. package/dist/transport/index.d.ts +10 -7
  84. package/dist/transport/mqtt/app-client-id.d.ts +16 -0
  85. package/dist/transport/mqtt/availability.d.ts +14 -0
  86. package/dist/transport/mqtt/bare-ip-tls.d.ts +46 -0
  87. package/dist/transport/mqtt/biz-stream.d.ts +98 -0
  88. package/dist/transport/mqtt/broker-discovery.d.ts +55 -0
  89. package/dist/transport/mqtt/clean-codec.d.ts +14 -0
  90. package/dist/transport/mqtt/command-router.d.ts +285 -0
  91. package/dist/transport/mqtt/dp-codec.d.ts +58 -0
  92. package/dist/transport/mqtt/dp-color.d.ts +14 -0
  93. package/dist/transport/mqtt/engine.d.ts +16 -0
  94. package/dist/transport/mqtt/index.d.ts +5 -0
  95. package/dist/transport/mqtt/secure-mqtt.d.ts +107 -0
  96. package/dist/transport/mqtt/topics.d.ts +80 -0
  97. package/dist/transport/p2p/adts.d.ts +91 -0
  98. package/dist/transport/p2p/annexb.d.ts +124 -0
  99. package/dist/transport/p2p/codec.d.ts +166 -0
  100. package/dist/transport/p2p/command-router.d.ts +655 -0
  101. package/dist/transport/p2p/commands.d.ts +550 -0
  102. package/dist/transport/p2p/envelope.d.ts +46 -0
  103. package/dist/transport/p2p/fmp4.d.ts +89 -0
  104. package/dist/transport/p2p/fragment-recording.d.ts +33 -0
  105. package/dist/transport/p2p/index.d.ts +13 -0
  106. package/dist/transport/p2p/lan-ip.d.ts +25 -0
  107. package/dist/transport/p2p/live-stream.d.ts +223 -0
  108. package/dist/transport/p2p/live-trace.d.ts +123 -0
  109. package/dist/transport/p2p/media.d.ts +105 -0
  110. package/dist/transport/p2p/p2p-session.d.ts +620 -0
  111. package/dist/transport/p2p/readable-egress.d.ts +27 -0
  112. package/dist/transport/p2p/session-manager.d.ts +154 -0
  113. package/dist/transport/p2p/shared-live-source.d.ts +431 -0
  114. package/dist/transport/p2p/talkback.d.ts +187 -0
  115. package/dist/transport/p2p/video.d.ts +150 -0
  116. package/dist/transport/p2p/write-commands.d.ts +21 -0
  117. package/dist/transport/protobuf.d.ts +5 -0
  118. package/dist/transport/push/fcm.d.ts +23 -0
  119. package/dist/transport/push/index.d.ts +6 -0
  120. package/dist/transport/push/message-tags.d.ts +26 -0
  121. package/dist/transport/push/parser.d.ts +27 -0
  122. package/dist/transport/push/proto.d.ts +11 -0
  123. package/dist/transport/push/push-client.d.ts +64 -0
  124. package/dist/transport/push/store.d.ts +23 -0
  125. package/dist/transport/push/types.d.ts +180 -0
  126. package/dist/transport/raw-dp.d.ts +6 -0
  127. package/dist/transport/stored-image-cache.d.ts +23 -0
  128. package/dist/transport/tuya/account.d.ts +44 -0
  129. package/dist/transport/tuya/client.d.ts +89 -0
  130. package/dist/transport/tuya/command-router.d.ts +79 -0
  131. package/dist/transport/tuya/dp-codec.d.ts +56 -0
  132. package/dist/transport/tuya/index.d.ts +28 -0
  133. package/dist/transport/tuya/request.d.ts +157 -0
  134. package/dist/transport/tuya/sign.d.ts +64 -0
  135. package/package.json +12 -13
  136. package/dist/client/index.js +0 -2
  137. package/dist/client/index.js.map +0 -1
  138. package/dist/core/index.js +0 -2
  139. package/dist/core/index.js.map +0 -1
  140. package/dist/model/index.js +0 -2
  141. package/dist/model/index.js.map +0 -1
  142. package/dist/transport/index.js +0 -2
  143. 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
+ [![npm](https://img.shields.io/npm/v/@mega-yfue/eufy-sdk?logo=npm&color=cb3837)](https://www.npmjs.com/package/@mega-yfue/eufy-sdk)
16
+ [![CI](https://github.com/mega-yfue/eufy-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/mega-yfue/eufy-sdk/actions/workflows/ci.yml)
17
+ [![node](https://img.shields.io/node/v/@mega-yfue/eufy-sdk?logo=nodedotjs)](./.nvmrc)
18
+ [![license](https://img.shields.io/npm/l/@mega-yfue/eufy-sdk)](./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()?.snapshot();
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-snapshot, one-shot mp4 record and WebRTC container-output paths use it.
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, webrtc
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`, `werift` — and that is deliberate. HTTP is native
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 affiliated
113
- with, endorsed by, or sponsored by Anker Innovations or eufy.** "eufy" and "Anker" are trademarks of
114
- their respective owners and appear here only to identify the hardware this SDK talks to. Use
115
- responsibly — rapid or failed logins can trigger a captcha or a temporary cooldown.
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
+ }