@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
@@ -0,0 +1,872 @@
1
+ /**
2
+ * eufy-sdk — one client for every eufy device class.
3
+ *
4
+ * Cloud APIs: the eufy v6 cloud (+ legacy, planned)
5
+ * Realtime: secure MQTT (appliances) + P2P (cameras/HomeBases)
6
+ *
7
+ * const eufy = new EufyMega({ email, password, region: "eu" });
8
+ * await eufy.login(); // → LoginResult; on success the SDK auto-starts realtime (push/MQTT/wired P2P)
9
+ * eufy.on("motion", (e) => console.log(e.deviceSn)); // typed semantic events — flowing already
10
+ * const dev = await eufy.getDevice((await eufy.getDevices())[0].sn);
11
+ * await dev.camera()?.snapshotStored();
12
+ *
13
+ * Connectivity is SDK-managed: the host calls no `connect*`. P2P to a battery camera is opened only
14
+ * when a command/stream/doorbell-ring needs it and closed when idle, so the camera can sleep.
15
+ */
16
+ import { EventEmitter } from "node:events";
17
+ import { MegaHttpClient, type LoginResult } from "../transport/http/mega-client.js";
18
+ import { type SecureMqttCredentials } from "../transport/mqtt/secure-mqtt.js";
19
+ import { type P2PSession } from "../transport/p2p/p2p-session.js";
20
+ import { type CleanRecordPage } from "../model/clean-records.js";
21
+ import { type AvailabilityObservation, type EufyDevice } from "../core/types.js";
22
+ import { Device, type DeviceInspection } from "../model/index.js";
23
+ import type { EufyMegaOptions, EufyMegaEvent, EufyMegaEventMap, DeviceState, RealtimeReadiness, WaitForRealtimeOptions } from "./types.js";
24
+ export type { EufyMegaOptions, EufyMegaEvent, EufyMegaEventMap, AnyDeviceEvent, DeviceState, RealtimePlaneReadiness, RealtimeReadiness, WaitForRealtimeOptions, } from "./types.js";
25
+ export interface EufyMega {
26
+ on<E extends EufyMegaEvent>(event: E, listener: (...args: EufyMegaEventMap[E]) => void): this;
27
+ once<E extends EufyMegaEvent>(event: E, listener: (...args: EufyMegaEventMap[E]) => void): this;
28
+ off<E extends EufyMegaEvent>(event: E, listener: (...args: EufyMegaEventMap[E]) => void): this;
29
+ emit<E extends EufyMegaEvent>(event: E, ...args: EufyMegaEventMap[E]): boolean;
30
+ }
31
+ /**
32
+ * The package entry point — one client per Anker eufy account. Handles the {@link login} state machine
33
+ * (captcha/2FA/persistence), resolves the account's devices ({@link getDevices}/{@link getDevice}
34
+ * → capability-driven {@link Device}s), **auto-manages** the realtime channels (FCM push + secure MQTT
35
+ * start on login; P2P opens on demand per station and idle-detaches battery cameras), and fans every
36
+ * transport's traffic into one typed semantic event stream (`eufy.on("motion", …)`). Construct it with
37
+ * an {@link EufyMegaOptions} (email/password + optional session/push stores), then drive `login()` to
38
+ * completion — no `connect*` call needed (set `autoRealtime:false` to opt out). Call {@link disconnect}
39
+ * to tear the realtime channels down.
40
+ */
41
+ export declare class EufyMega extends EventEmitter {
42
+ private readonly opts;
43
+ private readonly mega;
44
+ /**
45
+ * The installed secure-MQTT transports, one per credential scope.
46
+ *
47
+ * There is more than one because `get_user_mqtt_info` issues a DIFFERENT certificate per `app-name`
48
+ * and the broker's policy grants each line's topic space to its own cert — a `eufy_life` light's
49
+ * topics are silently denied to the default credential. A device's
50
+ * scope is a pure function of its record, so the set of live connections follows the roster with no
51
+ * extra state to keep in sync, and a scope with no devices is never connected.
52
+ */
53
+ private readonly transports;
54
+ /** Shared in-flight/settled `startMqtt()` promise PER SCOPE — so realtime bring-up and an on-demand
55
+ * publish await the SAME fully-connected transport instead of racing (and never open two). */
56
+ private readonly mqttReady;
57
+ /** Device list/record/capability resolution + the frame→caps cache. */
58
+ private readonly registry;
59
+ private pushClient?;
60
+ /** Push-fed passive image store; absent when the caller disables acquisition. */
61
+ private readonly storedImages?;
62
+ /** Account whose retained images are currently held. */
63
+ private storedImageAccount?;
64
+ /**
65
+ * Which bring-up generation is current. Bumped by every {@link disconnect}, so an in-flight
66
+ * {@link ensureRealtime} can tell on completion whether it is still the live one.
67
+ *
68
+ * The bring-up is fire-and-forget, so a caller that disconnects while it is still running would
69
+ * otherwise leave behind whatever opened after teardown had already passed. A single "closing" flag
70
+ * is not enough: `disconnect()` followed by `login()` clears it, and the stale bring-up then finishes,
71
+ * sees nothing amiss, and overwrites the live channels with its own — stranding the sockets it was
72
+ * meant to release. Comparing generations makes each bring-up responsible for exactly its own epoch.
73
+ */
74
+ private realtimeEpoch;
75
+ /** Shared startup state for the current epoch; caller timeouts never replace or cancel it. */
76
+ private realtimeGeneration?;
77
+ /**
78
+ * Devices already handed to a caller, so a realtime report refreshes the object they are holding
79
+ * rather than only the registry. Weak so a caller dropping a `Device` still lets it be collected —
80
+ * this map must never be what keeps one alive.
81
+ */
82
+ private readonly liveDevices;
83
+ /** Serialized state-transition transactions keyed by device and reflected member. */
84
+ private readonly stateTransitions;
85
+ /** Local writes already awaiting the same semantic transition, counted per reflected member. */
86
+ private readonly commandRefreshes;
87
+ /**
88
+ * The param ids each bound device's read getters were built from — the evidence the gate saw at bind
89
+ * time. Compared against an incoming report to notice when one carries an id the getters do not cover
90
+ * yet, which is the signal to rebuild them (see {@link rebindReads}).
91
+ */
92
+ private readonly boundParamIds;
93
+ /** Per-SKU DP catalog cache — keyed on model/T-code, fetched lazily via `get_product_data_point`. */
94
+ private readonly dpCatalogCache;
95
+ /** Semantic event names that speculatively pre-warm P2P (resolved once from the options; empty = off). */
96
+ private readonly prewarmEvents;
97
+ /** Station power tiers a pre-warm may open (resolved once from the options). */
98
+ private readonly prewarmTiers;
99
+ /** Transport-side owner of the P2P sessions + all wire senders. */
100
+ private readonly p2p;
101
+ /** Transport-side owner of the secure-MQTT ff09 lock/garage command path (sibling of {@link p2p}). */
102
+ private readonly mqtt;
103
+ /** Transport-side owner of the legacy Tuya REST command path for non-AIoT vacuums (G-series). */
104
+ private readonly tuya;
105
+ private readonly tuyaDpRouter;
106
+ /**
107
+ * Last state value announced per `deviceSn:event`, for the edge-trigger in {@link isRepeatState}.
108
+ * Realtime-only: the poll path deliberately bypasses it so an unchanged state is still re-asserted.
109
+ */
110
+ private readonly lastStateAnnounced;
111
+ /** Latest authoritative availability observation per device; no heuristic path writes this map. */
112
+ private readonly availabilityObservations;
113
+ /**
114
+ * One map per clean-line device, assembled from the pieces its `biz/…/res` frames carry.
115
+ *
116
+ * Created on the first frame that decodes rather than per device: a store for a robot that has never
117
+ * sent a map would answer `undefined` to everything, so allocating one buys nothing.
118
+ */
119
+ private readonly mapStores;
120
+ /** Re-armed after each cloud-param poll; cancelled by {@link disconnect}. */
121
+ private readonly pollTimer;
122
+ constructor(opts: EufyMegaOptions);
123
+ /**
124
+ * Builds the inbound listener that converts ThingClips DP maps to realtime capability state.
125
+ * DP values arrive as booleans, numbers, or strings; they are normalised to the string form
126
+ * the param store uses, then applied through the standard realtime-report path.
127
+ */
128
+ private makeTuyaDpInbound;
129
+ /**
130
+ * Reports what became of a command already acknowledged to its caller.
131
+ *
132
+ * A write whose declared observation never converged is not a fault of this client, so it does not reach
133
+ * the generic error bus: it is the answer to a question `dispatch` deliberately does not wait for, and it
134
+ * gets its own channel for exactly the reason `commandAck` has one. Anything else that goes wrong after
135
+ * the acknowledgement is a genuine fault and is reported as one.
136
+ */
137
+ private reportUnacknowledged;
138
+ /**
139
+ * Route an internal error to the host, without being able to kill it.
140
+ *
141
+ * A {@link SessionExpiredError} — a kicked/expired token, the transport having already cleared the
142
+ * session — is emitted as the dedicated `sessionExpired` event; it is NOT also sent to `error`. Every
143
+ * other error goes to `error`.
144
+ *
145
+ * Either way it falls back to a logged warning when nothing listens, because `error` on an
146
+ * `EventEmitter` THROWS when it has no listener, and most of these failures reach us from a
147
+ * fire-and-forget path (a transport callback, an un-awaited re-bind) where that throw would land as an
148
+ * unhandled rejection and abort the process.
149
+ *
150
+ * Only reported-error paths reach here — an error thrown straight out of a direct call is the caller's
151
+ * to handle.
152
+ */
153
+ private reportError;
154
+ /**
155
+ * The single entry point that installs the persistent secure-MQTT transport for one credential
156
+ * scope — used by BOTH the auto-realtime bring-up ({@link ensureRealtime}) and an on-demand light-DP
157
+ * publish (the router's `publishSecure`, wired in the constructor). Memoised per scope on
158
+ * {@link mqttReady} so the two can't race into two connections: whoever calls first starts it, the
159
+ * other awaits the same promise. Resolves only once {@link startMqtt} has `connect()`-ed, the
160
+ * transport is installed, and every device on that scope is subscribed — so a publish that awaits it
161
+ * always hits a live, subscribed client (never the silent no-op of firing into an unconnected
162
+ * transport). Epoch-guarded: if a {@link disconnect} lands mid bring-up, the freshly-connected
163
+ * transport is torn down instead of stranded. On failure the memo is cleared so a later call retries
164
+ * — but only if it is still THIS attempt's memo: a `disconnect()` clears the map and a later call can
165
+ * install a second bring-up, so an abandoned attempt that rejects afterwards must not wipe its
166
+ * successor (that would let two `connect()`s run concurrently and strand whichever installed first,
167
+ * still subscribed and double-emitting).
168
+ */
169
+ private ensureMqttStarted;
170
+ /**
171
+ * Give a realtime-only device a bounded chance to report before its actions are bound.
172
+ *
173
+ * The typed read getters are evidence-gated on the ids a device has reported, resolved once at bind
174
+ * time. A device whose state exists ONLY on its realtime wire therefore has no readable state at all
175
+ * if it is resolved before its first report. Waiting here is the cheap path: the returned `Device`
176
+ * already carries its reads, so a caller never has to watch for them appearing.
177
+ *
178
+ * It is a head start, not the mechanism — {@link rebindReads} installs reads that arrive later, which
179
+ * is what covers a device too slow (or too idle) to answer inside the window. A docked robot is
180
+ * exactly that.
181
+ *
182
+ * Skipped entirely for a device with a pollable cloud record, for an already-reporting device, and
183
+ * when realtime is off — so the common path adds nothing. Resolving on timeout leaves a silent device
184
+ * read-less rather than failing the lookup.
185
+ */
186
+ private awaitFirstRealtimeState;
187
+ /**
188
+ * Feed one map-stream frame to the device's map, and announce it if anything changed.
189
+ *
190
+ * Silent about a frame it cannot use. Most of them are: channels nothing reads yet, and fragments of
191
+ * a split message. Neither is a fault, and logging either would log on every frame of every clean.
192
+ */
193
+ private applyMapFrame;
194
+ private applyRealtimeReport;
195
+ /**
196
+ * Land state a capability recovered from a realtime signal: into the registry (so the next
197
+ * {@link getDevice} sees it) AND into any `Device` already handed out (so a caller holding one sees
198
+ * the new value without re-fetching). Announces every property whose value moved, then `deviceState`.
199
+ *
200
+ * Both writes matter: the registry alone would leave an existing `Device` stale until its freshness
201
+ * window expired, and that refresh re-reads the CLOUD record — which for a realtime-only line does
202
+ * not carry this state at all.
203
+ *
204
+ * This is three of the four inbound paths the security line has, and the ONLY one the clean and life
205
+ * lines have — a robot's cloud record carries none of its data points — so it is what brings those
206
+ * lines into scope for a property announcement at all. The announcement is edge-triggered for free:
207
+ * {@link Device.applyParams} names only the properties whose value actually moved, so a device
208
+ * re-reporting the same state is silent with no dedupe table to keep.
209
+ *
210
+ * The reported ids are recorded as evidence BEFORE the re-bind is fired, not after it lands. One
211
+ * report fans out to one call per capability that decoded it, and the re-bind is a cloud round-trip:
212
+ * advancing the set here is what stops the second call from firing a duplicate, and what stops a
213
+ * failed re-bind from re-triggering on every subsequent report.
214
+ */
215
+ private applyRealtimeState;
216
+ /**
217
+ * Re-install a bound device's read getters after a report widened the evidence.
218
+ *
219
+ * The getters are built ONCE, and only for params the device had already reported — so a line whose
220
+ * state arrives only over realtime binds with NO getters at all and would never grow them, no matter
221
+ * how much state landed afterwards. A robot vacuum is exactly that: its cloud record carries none of
222
+ * its data points, so the first report is what makes the reads exist.
223
+ *
224
+ * Re-binding is how the capability-gain path handles the same problem, and it is idempotent — the
225
+ * getters read live state through a closure over `getProperty`, so rebuilding them keeps every value
226
+ * a caller can already see. Only widening triggers it, so a device reporting the same ids repeatedly
227
+ * rebinds once.
228
+ *
229
+ * `deviceState` is re-emitted once the getters exist. The report that creates them is announced before
230
+ * they are installed, so the same event fires again when the reads are actually there, which is what
231
+ * makes "re-read on `deviceState`" true on the first report rather than only from the second.
232
+ * `bindActions` replaces the action objects, so the live ones are reached through the accessor
233
+ * (`dev.vacuumClean()`) and never through a bag cached earlier.
234
+ *
235
+ * The evidence set is widened, never replaced: the ids come back through the cloud record, and a
236
+ * record that omits a realtime-only id would otherwise un-know it and re-trigger on the next report.
237
+ */
238
+ private rebindReads;
239
+ /** The distinct credential scopes the current MQTT roster needs — no devices on a scope, no connection. */
240
+ private mqttScopesInUse;
241
+ /**
242
+ * Emit a semantic capability event whose name is only known at runtime (`decodeCapabilityEvent`
243
+ * returns a plain-string `event` + `payload`). The typed `emit` overload requires a literal event
244
+ * key, so this is the ONE place that bridges the dynamic name to the typed surface — every
245
+ * `emit:` string in a capability module is a member of {@link DeviceEventMap}, so the cast is
246
+ * sound. Keeping it here means the four dispatch loops stay a single call, not a scattered cast.
247
+ *
248
+ * `edge` asks for a state-carrying event to be suppressed when it only repeats what was last
249
+ * announced. The state is noted either way: the poll re-announces an unchanged value on purpose,
250
+ * and must still update what is known, or the next realtime signal carrying that same value would
251
+ * read as a change and be announced a second time.
252
+ */
253
+ private emitSemantic;
254
+ /**
255
+ * Await one capability-declared reflected param before publishing its valueless transition event.
256
+ *
257
+ * The state already on hand is consulted BEFORE fetching, but only where the observation carries a concrete
258
+ * value to compare against: a device that reports the written param on its own session lands it through
259
+ * {@link applyRealtimeState} within seconds, and polling the account device list to learn what the device has
260
+ * already said costs a dozen requests to reach the same answer. Without an expectation, "converged" means
261
+ * only "differs from what was read before", which state already on hand can satisfy spuriously — and the
262
+ * caller that has no expectation is the push path, where the signal itself is the news that a re-read is owed.
263
+ *
264
+ * The cloud half is asked for through {@link DeviceRegistry.refreshedList}, never by fetching the account
265
+ * list outright. The fetch is account-wide — one house list plus one device list per house — so a param that
266
+ * never converges would otherwise spend a whole burst of those every iteration of this loop, and concurrent
267
+ * transitions would multiply it by however many are in flight. The registry's reuse window and its
268
+ * single in-flight fetch collapse all of that to one list per window, shared across every waiter. The loop
269
+ * still turns on its own cadence: each pass re-reads what is known, so a value the device volunteers over
270
+ * its own session settles the wait between two cloud reads rather than after them.
271
+ */
272
+ private refreshEventState;
273
+ /** Serialize one complete state-transition transaction behind its keyed predecessor. */
274
+ private enqueueStateTransition;
275
+ /**
276
+ * Recycle a standalone device's P2P session after a write that needs one, waiting only
277
+ * {@link SESSION_RECYCLE_WAIT_MS} for it.
278
+ *
279
+ * The recycle itself waits for every viewer to detach, so that a write does not drop a live stream.
280
+ * That wait is unbounded by design — a viewer may watch indefinitely — and it happens INSIDE the keyed
281
+ * transaction, so the next write to the same member queues behind it. Racing it decouples the two: the
282
+ * losing recycle stays pending and still runs when the station falls idle, it just stops gating an
283
+ * unrelated write.
284
+ *
285
+ * A failure reported before the bound propagates; one arriving after it survives only as the session
286
+ * manager's own log, since by then nothing is waiting to receive it.
287
+ */
288
+ private recycleStandaloneSession;
289
+ /**
290
+ * Whether the DECODED property now reads what the write asked for.
291
+ *
292
+ * Compared against {@link CommandObservation.observed} where the property's decode is not the identity, and
293
+ * against the raw expectation only where the two coincide. A disable-bit param reports `0` for a property
294
+ * that reads `true`, so comparing the decoded value against the raw expectation would reject a write that
295
+ * had landed — the value converged and the transition event never fired.
296
+ */
297
+ private matchesObservation;
298
+ private eventRefreshKey;
299
+ /** Limit waiting on an unabortable dependency operation to the remaining semantic-event refresh window. */
300
+ private beforeDeadline;
301
+ /**
302
+ * Fan one inbound P2P frame out: raw escape hatch, device state, then semantic events.
303
+ *
304
+ * The frame is decoded against the capabilities of the device it came FROM, resolved from the
305
+ * `(station, channel)` pair — a shared command id (1700 serves pan-tilt, spotlight and privacy)
306
+ * would otherwise let an unrelated module's parser fabricate an event. The decode is a model
307
+ * concern kept on this side of the boundary, so the router never imports model.
308
+ *
309
+ * Frame events carry the station, which cannot say WHICH attached device reported when a station
310
+ * fans same-kind sensors out by channel, so the resolved serial is folded in; a payload's own
311
+ * fields still win on conflict.
312
+ *
313
+ * Params the station volunteered land as device state first. They arrive in the cloud record's own
314
+ * `param_type → value` shape, so no capability has to claim an id it does not own, and the reads
315
+ * they back stop waiting for the next cloud poll. A capability decode follows for the frames that
316
+ * report a bare value rather than that array, which the generic unwrap cannot recognise. Semantic
317
+ * events are edge-triggered: the same change also arrives as a push seconds later, and is announced
318
+ * once.
319
+ */
320
+ private onP2PFrame;
321
+ /**
322
+ * Record the state this signal reports, and say whether it merely repeats the last one announced.
323
+ *
324
+ * One physical change reaches the SDK on several transports — an entry sensor's contact arrives as
325
+ * a station notify ~2 s before the identical FCM push — and is announced once. The comparison is
326
+ * edge-triggered on the field a capability declared
327
+ * ({@link CapabilityModule.stateEvents}) rather than time-windowed: a genuine open→close→open burst
328
+ * differs from the last value at every step and passes intact, where any window wide enough to
329
+ * cover the transport spread would have swallowed the second open.
330
+ *
331
+ * Returns `false` — never a repeat — in the three cases where suppressing would lose information:
332
+ * an event no module declared as state-carrying (a pulse: motion, a doorbell press, whose
333
+ * consecutive occurrences are all real), a signal carrying no value for the field (it says nothing
334
+ * about the state, so it can neither duplicate nor overwrite it), and one that can't be attributed
335
+ * to a device (announcing twice beats suppressing a different device's change).
336
+ */
337
+ private noteState;
338
+ /**
339
+ * Begin (or resume) login. Returns a `LoginResult` — switch on `status`:
340
+ * - `ok` → authenticated (`result.session`).
341
+ * - `captcha` → show `result.image`, then {@link solveCaptcha}(answer).
342
+ * - `2fa` → a code was sent; {@link submitVerifyCode}(code).
343
+ *
344
+ * A restored session resolves straight to `ok`. No exceptions for the expected captcha/2FA flow.
345
+ *
346
+ * @example
347
+ * ```ts
348
+ * const res = await eufy.login();
349
+ * if (res.status === "captcha") await eufy.solveCaptcha(await ask(res.image));
350
+ * else if (res.status === "2fa") await eufy.submitVerifyCode(await ask());
351
+ * ```
352
+ */
353
+ login(opts?: {
354
+ messageType?: number;
355
+ }): Promise<LoginResult>;
356
+ /** Continue a `{status:"captcha"}` login with the solved answer. See {@link login}. */
357
+ solveCaptcha(answer: string, opts?: {
358
+ messageType?: number;
359
+ }): Promise<LoginResult>;
360
+ /** Continue a `{status:"2fa"}` login with the verify code that was sent. See {@link login}. */
361
+ submitVerifyCode(code: string): Promise<LoginResult>;
362
+ /**
363
+ * On a successful login, kick off auto-realtime (unless `autoRealtime:false`). Fire-and-forget so
364
+ * `login()` returns as soon as the session is ready — realtime channels come up in the background and
365
+ * surface failures via `error`. Idempotent through the retained generation promise.
366
+ */
367
+ private afterLogin;
368
+ /**
369
+ * Wait for the auto-managed realtime startup begun by the current successful {@link login}.
370
+ *
371
+ * A caller-specific timeout does not cancel startup. Calls made before successful login reject with
372
+ * `login() first`; clients configured with `autoRealtime:false` resolve as `disabled` without opening
373
+ * a transport.
374
+ */
375
+ waitForRealtime(options?: WaitForRealtimeOptions): Promise<RealtimeReadiness>;
376
+ /** Raw mega HTTP client, for endpoints not yet wrapped. */
377
+ get api(): MegaHttpClient;
378
+ /**
379
+ * Fetch the per-user secure-MQTT credentials for realtime appliance control. Pass `appName` to
380
+ * request a specific capability scope on the current session without re-logging in — security
381
+ * devices (locks/garage) need the `eufy_security` scope, which the default scope can't reach.
382
+ */
383
+ getUserMqttInfo(appName?: string): Promise<SecureMqttCredentials>;
384
+ /**
385
+ * A {@link CommandSink} bound to one device serial. This is the one place a device's commands fan out
386
+ * to whichever transport it actually has — so capability modules and `getDevice` callers never need to
387
+ * know which, and this facade names no capability and builds no wire bytes itself. The `ff09-*` kinds
388
+ * ride ONE shared frame over BOTH P2P and secure-MQTT, so they're routed by the device's topology
389
+ * (`p2p_did` present → P2P, else MQTT); the chosen router re-resolves its own routing tail. Every
390
+ * other kind is P2P.
391
+ */
392
+ private commandSinkFor;
393
+ /**
394
+ * Combine explicit P2P media with the optional passive push-thumbnail provider.
395
+ *
396
+ * The retained still also becomes the answer for a live still that could not be captured. A station
397
+ * serves one camera at a time and a live view outranks a tile, so a still asked for while a sibling is
398
+ * being watched is refused at the transport. Answering the retained bytes answers the read rather than
399
+ * failing it, marked {@link MediaProvider.snapshotLive} `retained` so the caller knows they are not
400
+ * current. With nothing retained the refusal stands.
401
+ */
402
+ private mediaProviderFor;
403
+ /**
404
+ * Choose the transport stack for one command — the routing half of {@link commandSinkFor}.
405
+ *
406
+ * The `ff09-*` kinds share ONE frame that rides either transport, so each stack is asked whether it
407
+ * drives this device (`claimsDevice`) rather than inferring from the kind. `registry.require` throws
408
+ * on an unloaded serial, so a routing decision never silently falls through to the wrong transport.
409
+ * The `eufy_life` DP writes (smart lights) are secure-MQTT-only. `aiot-dp` routes to either the
410
+ * Anker AIoT MQTT stack or the legacy Tuya REST router depending on the device's category
411
+ * (`eufy_home_tuya` → Tuya, everything else → MQTT). The capability layer emits a single `aiot-dp`
412
+ * kind and stays transport-agnostic; only the facade sees both sides and decides here. Everything
413
+ * else is P2P.
414
+ */
415
+ private routeCommand;
416
+ /**
417
+ * List + classify devices across all houses (mega API). Each device is tagged with its API backend
418
+ * + realtime transport. Camera/HomeBase records still appear here for inventory; driving them is
419
+ * P2P. Delegates to `DeviceRegistry` (the house-scoped merge/dedupe lives there).
420
+ *
421
+ * Side-effect: registers `eufy_home_tuya` devices with the Tuya command router so
422
+ * the command dispatcher can resolve a eufy SN → Tuya devId without a separate lookup.
423
+ * The Tuya id is extracted from the device's raw cloud record (`tuya_uuid`, `tuya_virtual_id`,
424
+ * `tuya_device_id`, or `virtualId` fields — whichever is non-empty).
425
+ *
426
+ * A partial cloud outage still resolves, with the devices that answered plus the ones already known — but a
427
+ * session the cloud has rejected REJECTS, with {@link SessionExpiredError}. An empty list would be
428
+ * indistinguishable from an account with no devices.
429
+ */
430
+ getDevices(): Promise<EufyDevice[]>;
431
+ /** Devices that this client drives over MQTT (transport ≠ p2p). */
432
+ getMqttDevices(): EufyDevice[];
433
+ /**
434
+ * One page of a robot vacuum's **cleaning history**, in whatever order the cloud returns it —
435
+ * newest first in practice, but that is the gateway's contract and the SDK does not re-sort.
436
+ *
437
+ * `pageSize` is how many records to return and `page` is 1-based; page through until the returned
438
+ * `total` is reached. Answers an empty page rather than throwing when the account has no history for
439
+ * the device or the response cannot be read.
440
+ *
441
+ * Each record carries a `downloadUrl` for the run's binary detail blob (map and per-run statistics).
442
+ * The SDK hands that URL over rather than fetching it — the host is unconfirmed and the blob's format
443
+ * is not evidenced yet.
444
+ */
445
+ getCleanRecords(deviceSn: string, pageSize?: number, page?: number): Promise<CleanRecordPage>;
446
+ /**
447
+ * Inspect one device by serial: resolve its codec/capabilities, cross-reference every reported
448
+ * `param_type` against the param dictionary, and emit a paste-ready `registry.ts` row plus
449
+ * dictionary snippets for anything unknown. Loads the device list if needed; prefers
450
+ * the live `get_device_param_list` for freshest params, falling back to the device-list params.
451
+ */
452
+ inspectDevice(sn: string): Promise<DeviceInspection>;
453
+ /**
454
+ * The device's LIVE, authoritative `rtsp://` URL — host, path, and the credentials it is
455
+ * enforcing right now — or `undefined` when none is pushed within the read window.
456
+ *
457
+ * A thin public door onto the P2P transport (which stays internal otherwise): opens the
458
+ * station's session on demand, so a viewer adopting a tile can call this directly without one
459
+ * already existing. It writes only the publish switch and the test-stream provoke, never the
460
+ * credentials, so a stream a NAS/NVR already consumes keeps its own pair.
461
+ *
462
+ * This is the CANONICAL way to fetch the URL: it provokes and returns it. The `rtsp` capability's
463
+ * `url` member surfaces the SAME value as inbound state for code that already holds a `dev.rtsp()`
464
+ * and reacts to `propertyChanged` — not a second way to fetch it.
465
+ *
466
+ * Every failure — no route, no account id, level-2 not ready, no push before the deadline — collapses
467
+ * to `undefined`. The distinction the caller might want (terminal "no RTSP" vs a transient "session
468
+ * not warm yet") is not drawn here yet; a caller that retries on `undefined` recovers from the
469
+ * transient case. The read window is a fixed 12 s — long enough for a cold HomeBase to wake and
470
+ * answer, and about the ceiling a UI adopting a tile will wait — deliberately not caller-tunable.
471
+ */
472
+ reportedRtspUrl(sn: string): Promise<string | undefined>;
473
+ /**
474
+ * Build a live {@link Device} model object for one serial: the resolved codec/capabilities with
475
+ * its current param values applied (named via the param dictionary; unknown ids kept as
476
+ * `unknown_<pt>`). This is the device primitive — `dev.getProperties()`, `dev.has(cap)`, etc.
477
+ * Prefers fresh `get_device_param_list`, falls back to the device-list params.
478
+ * Under auto-realtime the returned Device is wired with a read-through freshness cache (see
479
+ * {@link Device.setFreshnessPolicy}), so repeat reads are served from cache instead of re-fetching,
480
+ * and realtime updates keep values fresh.
481
+ *
482
+ * That refresh ANNOUNCES what it lands, like the other two inbound paths. Under frequent reads it
483
+ * fires every `cacheTtlMs` where the poll fires every ten minutes, so it is where most fresh cloud values
484
+ * arrive — and each announcing path is edge-triggered on the same live state, so whichever sees a change
485
+ * first announces it and the others stay silent. Its timing says only when a caller happened to read; the
486
+ * value is the news. It applies what the device volunteered over realtime on top of the cloud half, which
487
+ * the registry keeps apart, so it can neither revert nor announce a revert of a report already landed.
488
+ *
489
+ * The `Device` returned is held WEAKLY: it is what the inbound paths announce against, so a caller that
490
+ * wants property changes for a serial keeps its own reference. Dropping it stops the announcements, not
491
+ * the device.
492
+ *
493
+ * @example
494
+ * ```ts
495
+ * const dev = await eufy.getDevice(sn);
496
+ * if (dev.has("camera")) await dev.camera()?.snapshotStored();
497
+ * console.log(dev.getProperty("battery"));
498
+ * ```
499
+ */
500
+ getDevice(sn: string): Promise<Device>;
501
+ /**
502
+ * The {@link Ff09SettingsReader} behind `dev.lock()?.getAutoLockState()` — picks P2P vs MQTT the same
503
+ * way {@link commandSinkFor} does for writes, so `lock.ts` never has to know which transport this
504
+ * device has. `undefined` when `ctx.adminUserId` is missing (not a lock-family device — mirrors
505
+ * `CommandContext.adminUserId`'s own doc: "present on lock-family devices, absent elsewhere").
506
+ *
507
+ * It earns its own boundary because `GET_SETTINGS` is a request/reply query that neither
508
+ * `CommandSink` (write-only) nor `MediaProvider.p2pQuery` (P2P-only, no decrypt) fits. Named for the
509
+ * frame family rather than the capability, so this facade stays capability-neutral like the layers
510
+ * below it — see `CapabilityModule.actions`'s doc before adding another.
511
+ */
512
+ private ff09SettingsReaderFor;
513
+ /**
514
+ * Auto-realtime bring-up — the SDK owns connectivity so the host calls no `connect*`. Runs once per
515
+ * session after a successful login (unless `autoRealtime:false`). Starts the **always-on, battery-safe**
516
+ * channels — FCM push (account-wide events) + secure MQTT (iff appliances present) — and eagerly warms
517
+ * P2P **only for wired stations** (HomeBases / mains cameras, which don't drain). Battery cameras are
518
+ * left detached: their P2P opens on demand (command / stream, or an opted-in event pre-warm) and
519
+ * idle-detaches. All channels start concurrently; a single failure surfaces via `error` without
520
+ * aborting the rest.
521
+ */
522
+ private ensureRealtime;
523
+ /** Create or join the one transport bring-up owned by the current realtime epoch. */
524
+ private ensureRealtimeGeneration;
525
+ /** Start all selected transports concurrently and settle their owning generation once. */
526
+ private startRealtimeGeneration;
527
+ /** Resolve a generation with an immutable count snapshot; later transport completions are ignored. */
528
+ private settleRealtimeGeneration;
529
+ /**
530
+ * Arm the next cloud-param poll. Re-arms from the END of each run rather than on a fixed interval,
531
+ * so a slow list fetch can never stack overlapping polls, and re-arms in `finally` so a failed poll
532
+ * (a transient cloud error) doesn't silently kill the loop for the session's remaining life.
533
+ *
534
+ * Disabled by `pollMs: 0`, and never started when `autoRealtime:false` — a host that opted out of
535
+ * SDK-managed connectivity gets no background traffic. Also declines to re-arm once the bring-up
536
+ * that armed it has been superseded, so a poll that fires as the client shuts down can't resurrect
537
+ * the loop after teardown.
538
+ */
539
+ private schedulePoll;
540
+ /** The effective cloud poll interval in ms — the configured {@link EufyMegaOptions.pollMs} or the default. */
541
+ get pollIntervalMs(): number;
542
+ /**
543
+ * Change the cloud poll interval at runtime; `ms` is the gap between polls, `0` disables polling.
544
+ *
545
+ * Takes effect immediately: the pending tick is cancelled and the loop re-armed at the new interval
546
+ * (or left cancelled for `0`). Unlike the constructor {@link EufyMegaOptions.pollMs}, this can be
547
+ * changed after login.
548
+ */
549
+ setPollInterval(ms: number): void;
550
+ /**
551
+ * One poll pass: re-read the device list, land what moved on the live devices, announce every property
552
+ * whose value changed, and emit a semantic event for every param that changed value since the last pass.
553
+ *
554
+ * `propertyChanged` is the generic channel this exists for: most readable members arrive only as a
555
+ * cloud param and no push carries them, so re-reading was the only way a caller could learn one had
556
+ * moved and re-reading cannot say WHEN. A capability's own `source:"poll"` mapping is beside it, for a
557
+ * state that carries something a bare property change cannot (`contact.ts` maps the contact param as a
558
+ * third transport for a state its push and its station notify also report).
559
+ *
560
+ * Each change is decoded against the reporting device's capabilities, the same argument the push path
561
+ * passes: a param id claimed by more than one capability cannot be resolved without it, so a poll
562
+ * event declared on a contested id would be declared and then silently never emitted.
563
+ *
564
+ * Also emits `deviceState` for each device the diff reports as having re-reported. That is tracked
565
+ * apart from the param diff because the two are different facts: the cloud can re-stamp a param with
566
+ * an unchanged VALUE, which is no state change to report but is fresh proof the device is alive. A
567
+ * device absent from the previous pass is skipped — first sight is discovery, not a transition;
568
+ * {@link deviceState} answers an initial reading.
569
+ */
570
+ private pollOnce;
571
+ /**
572
+ * Land a poll pass's CHANGES on every live {@link Device} and announce what moved, BEFORE anything
573
+ * else derived from them is emitted.
574
+ *
575
+ * Ordered that way because live state is the map every capability getter reads: a listener reading a
576
+ * getter inside a poll event handler has to see the value that event is about. The read-through
577
+ * freshness policy cannot stand in for this — it fires on a READ of a stale value and hands that read
578
+ * the stale one, so a value nothing happens to read is never refreshed by it.
579
+ *
580
+ * The CHANGES, not the whole post-change map {@link ParamChange} also carries. That map is there so an
581
+ * event decode can read sibling params; applying it would revert every id a realtime report made
582
+ * fresher, because {@link DeviceRegistry.applyRealtimeParams} keeps a report apart from the cloud
583
+ * record's params — the cloud list carries the pre-report value long after the device volunteered the
584
+ * new one, so an open door reads as closed on the next pass that sees anything on that device move.
585
+ *
586
+ * That precedence is the reason a moved id is also RETIRED from the report map
587
+ * ({@link DeviceRegistry.retireRealtimeParams}). The report outranks the cloud only while it is the
588
+ * fresher half, and a diff on that id is the cloud stating a transition of its own — so left in place
589
+ * the report would outrank it forever, and the next join of the two halves would revert this pass's
590
+ * value and announce the revert. Retired for EVERY device the diff touched, not only a live one: the
591
+ * join also feeds the `Device` a later {@link getDevice} builds, which no live entry exists for yet.
592
+ */
593
+ private applyPolledParams;
594
+ /**
595
+ * The live {@link Device} for a serial, for a path that is about to ANNOUNCE against it — reporting
596
+ * once when one the caller asked for has since been collected.
597
+ *
598
+ * An announcement carries the value read out of that device's own live state, so a collected device
599
+ * cannot be announced for, and the caller is the only thing keeping one alive — {@link liveDevices} is
600
+ * weak by contract. Losing announcements that way fails in the three worst ways at once: it is
601
+ * non-deterministic (it turns on when the collector runs, so it holds in development and stops under
602
+ * memory pressure), silent (no error, the events simply cease), and non-local (the obligation is on
603
+ * {@link getDevice}, the symptom shows on `propertyChanged`).
604
+ *
605
+ * Neither alternative is available: re-deriving the value outside live state is two answers for one
606
+ * reading, which is the disagreement the announcement exists to remove, and keeping every device alive
607
+ * here reverses this map's own invariant. So it is LOUD.
608
+ *
609
+ * Reported only for a serial the caller DID ask for, since one never fetched has no object by definition
610
+ * and was never owed an announcement — reporting those would name most of the account on every pass. The
611
+ * dead entry is dropped as it is reported, which is what makes it once: a device let go on purpose must
612
+ * not narrate every inbound signal for the rest of the session, and a later {@link getDevice} re-registers
613
+ * the serial and resumes announcing.
614
+ *
615
+ * Deliberately not routed through {@link reportError}: nothing in this SDK failed, so it must not reach a
616
+ * host's `error` handling. It is a usage fact, reported at `warn`.
617
+ */
618
+ private liveDeviceToAnnounce;
619
+ /**
620
+ * Apply a param map to one live {@link Device} and announce every property it moved, one
621
+ * `propertyChanged` each. The one place the two halves are joined, shared by all three inbound paths
622
+ * that reach live state.
623
+ *
624
+ * The device decides which of the changed names it will stand behind and what value each carries
625
+ * ({@link Device.announcements}), so this stays a fan-out: no capability name, no member id, and no
626
+ * second conversion of a wire value that could disagree with the getter beside it.
627
+ *
628
+ * Only a device a caller is HOLDING is announced for, because the announced value is read out of that
629
+ * device's own live state and a serial nobody asked for has none. Resolving one on demand could not
630
+ * help: a device built from the already-updated record has nothing to diff against, so the pass that
631
+ * created it could never be the pass it announces. Such a device's liveness still reaches a host as
632
+ * `deviceState`.
633
+ *
634
+ * Echoes of the SDK's own writes are announced rather than suppressed. An inbound path cannot tell a
635
+ * change it caused from one an external actor caused, and suppressing on that guess is unsound, not
636
+ * merely conservative: if a user also changes the value in the vendor app inside the window, the real
637
+ * external change is the one lost — a wrong state held indefinitely, against one redundant idempotent
638
+ * re-read.
639
+ */
640
+ private applyAndAnnounce;
641
+ /**
642
+ * Re-resolve a device a caller is holding, in case fresher evidence granted it a capability.
643
+ *
644
+ * A capability is granted on evidence the device reports, so one resolved before it had reported a
645
+ * param lacks the capability that param proves — permanently, for that object, even once the value
646
+ * starts arriving. This closes the gap for the `Device` instances already handed out: on new
647
+ * evidence they gain the accessor, bound, without the caller re-fetching.
648
+ *
649
+ * Only widens, never retracts, and re-binds only when something was actually gained, so the common
650
+ * poll costs one set comparison. Best-effort: a device that has been dropped, or a re-bind that
651
+ * fails, must not break the poll loop for every other device.
652
+ *
653
+ * The whole record goes in, never a field-by-field copy of it: since this path only ever ADDS, a
654
+ * field left behind here re-grants what the first resolution deliberately withheld — an attached
655
+ * camera would take back the hub's guard mode on the first param change the poll saw.
656
+ */
657
+ private widenCapabilities;
658
+ /**
659
+ * Open the P2P session for ONE device's station, and nothing else.
660
+ *
661
+ * Auto-realtime warms every wired station on the account, which is what a host driving a
662
+ * fleet wants. A caller that needs exactly one station does not: an unreachable station broadcasts a
663
+ * local lookup to `255.255.255.255` **once a second for the full connect timeout** and sends a PPCS
664
+ * lookup to every cloud address in the same tick, so warming a fleet to talk to one camera is a
665
+ * burst of broadcast and NAT churn on the user's network for stations nobody asked about. Pair this
666
+ * with `autoRealtime: false` to open only what is being used.
667
+ *
668
+ * Resolves when the station is connected; rejects on its connect timeout. Best-effort and idempotent
669
+ * — an already-open session resolves immediately.
670
+ */
671
+ connectStation(deviceSn: string, signal?: AbortSignal): Promise<void>;
672
+ /** Eagerly open P2P sessions for WIRED stations only (persistent — they don't drain). Battery
673
+ * stations stay closed until an on-demand open. Best-effort per station. */
674
+ private warmWiredP2P;
675
+ /**
676
+ * A station's power tier for the P2P lifecycle: a HomeBase/station is `"wired"` (persistent); a
677
+ * standalone device is `"battery"` iff its resolved capabilities include `battery`, else `"wired"`.
678
+ * Keyed on the STATION's own power, never a child's (a battery cam attached to a wired HomeBase draws
679
+ * from the base's persistent session). Reads capabilities on the client side — no model type leaks to
680
+ * transport (the router only ever sees the `"wired"|"battery"` string).
681
+ */
682
+ private stationPower;
683
+ /**
684
+ * Speculatively open the P2P session of the station behind `deviceSn`, if the caller opted this
685
+ * semantic event in — so a stream or talkback opened right after a doorbell ring or a detection starts
686
+ * warm instead of paying a cold open.
687
+ *
688
+ * Four gates. `autoRealtime: false` means the SDK opens nothing on its own initiative at all;
689
+ * {@link EufyMegaOptions.prewarmEvents} must name the event, and it names none by default, which is
690
+ * what makes pre-warm opt-in; the station must be one the account actually reports; and its power tier
691
+ * must be one {@link EufyMegaOptions.prewarmTiers} allows.
692
+ *
693
+ * The tier is resolved for the STATION whose session would open, which is why an attached camera is
694
+ * judged by its base — {@link P2PCommandRouter.stationKeyOf} is the single source of that mapping, and
695
+ * {@link stationPower} of the tier. A station with no record of its own is declined rather than
696
+ * pre-warmed: {@link stationPower} answers `"wired"` for one it cannot find, because the tier it feeds
697
+ * the session lifecycle must always be an answer — and taking that answer here is how a battery camera
698
+ * gets pre-warmed under a `"wired"`-only opt-in.
699
+ *
700
+ * Best-effort and unawaited: a pre-warm nobody uses must cost the caller nothing, so a failed open
701
+ * surfaces on `error` like any other background transport failure.
702
+ */
703
+ private prewarmForEvent;
704
+ /**
705
+ * Connect a secure-MQTT transport for one credential scope (appliances: vacuum, light, plug,
706
+ * display) using credentials fetched from the cloud, and return it **connected but not installed**:
707
+ * {@link ensureMqttStarted} owns installing it, subscribing devices, and the epoch check, so that
708
+ * lifecycle lives in exactly one place. Only ever called through {@link ensureMqttStarted}.
709
+ *
710
+ * The inbound decode is gated by the reporting device's own capabilities, so one line's decoder never
711
+ * runs against another's traffic, and the DP frame is unwrapped here — the layer that may import the
712
+ * transport — so a capability reads tags without owning any framing.
713
+ */
714
+ private startMqtt;
715
+ /**
716
+ * Subscribe the devices on one credential scope; a failing subscribe is reported, not fatal (one
717
+ * unreachable device must not stop the rest of the roster from coming up).
718
+ */
719
+ private subscribeMqttDevices;
720
+ /**
721
+ * Send whatever a device's capabilities want sent once its realtime channel is up — for a line whose
722
+ * state is pushed on change with no heartbeat, the request that makes its state readable before the
723
+ * first write. Best-effort: reported, never fatal, since a device that ignores it is only left with
724
+ * the state it would have had anyway.
725
+ */
726
+ private sendRealtimeInit;
727
+ /**
728
+ * Stations with a live P2P session. P2P is auto-managed: wired stations are warmed at login, battery
729
+ * stations open on demand (command / stream, or an opted-in event pre-warm) and idle-detach — so this
730
+ * map grows and shrinks over time. `p2pConnect(stationSn)` / `p2pClose(stationSn)` events track the
731
+ * changes.
732
+ */
733
+ getP2pSessions(): Map<string, P2PSession>;
734
+ /**
735
+ * The liveness facts for one device — see {@link DeviceState}. Facts, not an `online` verdict: "how
736
+ * long is too long" is a threshold that belongs to the caller, and it differs per device class.
737
+ *
738
+ * The `deviceState` event announces when a device reports in.
739
+ */
740
+ deviceState(sn: string): DeviceState;
741
+ /**
742
+ * Return the latest explicit availability observation for `sn`, or `undefined` when no verified
743
+ * vendor signal has been observed. This never derives a state from {@link DeviceState.lastSeenMs},
744
+ * connection silence, P2P lifecycle, operation failures or caller-selected timeouts.
745
+ */
746
+ deviceAvailability(sn: string): AvailabilityObservation | undefined;
747
+ /** Decode a verified wire signal, then assign its device-availability semantics at the client seam. */
748
+ private processAvailabilityMessage;
749
+ /**
750
+ * Retain one authoritative observation per device and emit only state transitions. When both the
751
+ * previous and incoming envelopes supply ordering evidence, an older message cannot overwrite newer
752
+ * device truth. Exact duplicate ordering cannot reverse state. If comparable vendor ordering is
753
+ * absent, handler arrival order defines which explicit observation is later. A same-state observation
754
+ * still refreshes the retained evidence without re-emitting.
755
+ */
756
+ private applyAvailabilityObservation;
757
+ /**
758
+ * The {@link DeviceState} for a device record already in hand — the shape {@link deviceState} returns
759
+ * once it has resolved the serial, reused by the poll loop so emitting for a batch of devices doesn't
760
+ * re-scan the roster per device.
761
+ */
762
+ private stateOf;
763
+ /**
764
+ * The capability set to disambiguate an inbound push/poll id with, or `undefined` when the serial is
765
+ * unknown. Push ids are namespaced per device family, so the same integer means different things on
766
+ * different hardware; `decodeEvent` needs the device's capabilities to pick the right mapping and
767
+ * deliberately stays silent rather than guessing when it can't.
768
+ */
769
+ private capsForEvent;
770
+ /**
771
+ * **Write** a device property. Asks the capability modules to build the command for this
772
+ * `(name, value)` — the module owns how THIS device applies it. No `(name, value)` recipe → the
773
+ * device doesn't support the property, so we throw {@link CapabilityNotSupportedError} rather than
774
+ * a silent no-op. On success the device echoes the new state back as a param update — read it with
775
+ * {@link getDevice} to confirm.
776
+ *
777
+ * @param sn device serial.
778
+ * @param name property name (e.g. "light", "brightness", "enabled").
779
+ * @param value desired value.
780
+ *
781
+ * @example
782
+ * ```ts
783
+ * await eufy.setProperty(sn, "brightness", 50);
784
+ * await eufy.setProperty(sn, "light", true);
785
+ * ```
786
+ */
787
+ setProperty(sn: string, name: string, value: boolean | number | string): Promise<void>;
788
+ /**
789
+ * Restart a HomeBase.
790
+ *
791
+ * **HomeBases only** — restart is a hub operation, so a non-HomeBase serial (a camera, an NVR)
792
+ * throws rather than doing nothing. The hub drops its connection and returns after a minute or two,
793
+ * so everything behind it is briefly offline. Verified on real hardware.
794
+ */
795
+ reboot(sn: string): Promise<void>;
796
+ /**
797
+ * Build the {@link CommandContext} for a device: the evidence a capability uses to resolve a
798
+ * command variant (channel, codec, deviceType, model, reported param/DP ids) plus the RESOLVED
799
+ * capability set that gates command building.
800
+ *
801
+ * Resolves from the same fresh `DeviceRegistry.record` (`get_device_param_list` overlay +
802
+ * category) and `resolveDevice` that {@link getDevice} uses, so the capability set here is byte-for-byte
803
+ * what `device.has(cap)` / `buildActions` saw — a command is never rejected for a capability the
804
+ * device model advertises.
805
+ *
806
+ * `paramIds` is cloud params UNION whatever the device reported over realtime — it is the evidence
807
+ * gate behind the typed read getters, so a line whose state only ever arrives live still advertises
808
+ * exactly the reads it has.
809
+ */
810
+ private fetchDpCatalog;
811
+ private commandContext;
812
+ /**
813
+ * **Generic P2P request/reply query** — sends a `SET_PAYLOAD` sub-command and resolves with the
814
+ * reply frame's `payload`. Transport-only escape hatch (the router owns the wire); the caller owns
815
+ * the sub-command id and the reply shape (e.g. the doorbell's 6237 quick-response list).
816
+ * @internal
817
+ */
818
+ p2pQuery(sn: string, subCmd: number, opts?: {
819
+ timeoutMs?: number;
820
+ }): Promise<Record<string, unknown>>;
821
+ /**
822
+ * Connect FCM push — the always-on, server-initiated channel that delivers event + **thumbnail**
823
+ * notifications (motion/person/doorbell/package, each with a thumbnail URL). Independent of
824
+ * MQTT/P2P. On first run it registers a push token, tells the eufy cloud to push to it, then holds
825
+ * the socket. With a `pushStore`, the token + seen ids persist so later runs just reconnect. Emits:
826
+ * - `push(event)` — normalised `PushEvent` (deviceSn, eventType, eventName,
827
+ * thumbnailUrl, cipher, payload, raw)
828
+ * - `pushRaw(raw)` — the raw `RawPushMessage`
829
+ * - `pushConnect` / `pushDisconnect`
830
+ *
831
+ * Started automatically by {@link ensureRealtime} after login. A semantic event the caller opted into
832
+ * via {@link EufyMegaOptions.prewarmEvents} — none by default — also speculatively pre-warms that
833
+ * camera's P2P session, so a following stream or talkback starts warm; {@link prewarmForEvent} owns that
834
+ * decision, and the router stays event-agnostic.
835
+ *
836
+ * Returns the connected client rather than installing it. Registration can outlive a `disconnect()`,
837
+ * and {@link ensureRealtime} owns the decision of whether a finished bring-up is still the current
838
+ * one — so this never overwrites a channel a later login already brought up.
839
+ */
840
+ private startPush;
841
+ /** Admit only exact, account-known devices with resolved snapshot evidence into the passive store. */
842
+ private observeStoredImage;
843
+ /**
844
+ * Tear down every realtime channel: close the secure-MQTT transport, all P2P sessions, and the FCM
845
+ * push socket. Idempotent — safe to call when nothing is connected. Leaves the login session intact
846
+ * (call {@link login} again to reconnect without re-authenticating).
847
+ */
848
+ disconnect(): Promise<void>;
849
+ /**
850
+ * Disconnect and forget every installed secure-MQTT transport.
851
+ *
852
+ * Clearing both installed transports and their in-flight memos lets the next successful login own a
853
+ * fresh set. Individual stale attempts retain their epoch guard and close only the transport they
854
+ * created, so they cannot clear a successor generation's map.
855
+ */
856
+ private closeMqttTransports;
857
+ /**
858
+ * Close every realtime channel and stop the poll loop.
859
+ *
860
+ * Safe to run twice: each channel is cleared as it closes. The edge-trigger's memory of announced
861
+ * states goes with them: it describes what was announced over a connection that no longer exists,
862
+ * and keeping it would suppress the first report after a reconnect as a duplicate — leaving nothing
863
+ * announced until the state next physically changes.
864
+ */
865
+ private teardownRealtime;
866
+ /** True if a usable session (restored from store or freshly logged in) is held. */
867
+ get loggedIn(): boolean;
868
+ /** Tear down realtime, clear passive media, and forget the persisted login session. */
869
+ logout(): Promise<void>;
870
+ /** Forget the persisted session (forces a fresh login + 2FA next time) and clear account-owned media. */
871
+ clearSession(): void;
872
+ }