@mega-yfue/eufy-sdk 0.0.4 → 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -12
- package/dist/client/device-registry.d.ts +343 -0
- package/dist/client/eufy-mega.d.ts +872 -0
- package/dist/client/index.d.ts +1 -6
- package/dist/client/map-channels.d.ts +22 -0
- package/dist/client/types.d.ts +383 -0
- package/dist/core/contracts.d.ts +898 -0
- package/dist/core/crypto.d.ts +98 -0
- package/dist/core/index.d.ts +9 -7
- package/dist/core/logger.d.ts +53 -0
- package/dist/core/lz4-block.d.ts +35 -0
- package/dist/core/raw-dp-hex.d.ts +32 -0
- package/dist/core/raw-dp-writer.d.ts +83 -0
- package/dist/core/store.d.ts +43 -0
- package/dist/core/types.d.ts +169 -0
- package/dist/core/util.d.ts +78 -0
- package/dist/index.d.ts +4 -7
- package/dist/index.js +25526 -14
- package/dist/index.js.map +7 -1
- package/dist/model/capabilities/access.d.ts +127 -0
- package/dist/model/capabilities/arming.d.ts +201 -0
- package/dist/model/capabilities/audio.d.ts +154 -0
- package/dist/model/capabilities/battery.d.ts +363 -0
- package/dist/model/capabilities/camera.d.ts +564 -0
- package/dist/model/capabilities/co.d.ts +40 -0
- package/dist/model/capabilities/contact.d.ts +133 -0
- package/dist/model/capabilities/doorbell.d.ts +345 -0
- package/dist/model/capabilities/dp-catalog.d.ts +38 -0
- package/dist/model/capabilities/index.d.ts +561 -0
- package/dist/model/capabilities/info.d.ts +28 -0
- package/dist/model/capabilities/keypad.d.ts +61 -0
- package/dist/model/capabilities/leak.d.ts +43 -0
- package/dist/model/capabilities/light.d.ts +174 -0
- package/dist/model/capabilities/locate.d.ts +63 -0
- package/dist/model/capabilities/lock.d.ts +242 -0
- package/dist/model/capabilities/manifest.d.ts +107 -0
- package/dist/model/capabilities/members.d.ts +647 -0
- package/dist/model/capabilities/motion.d.ts +377 -0
- package/dist/model/capabilities/person-detection.d.ts +8 -0
- package/dist/model/capabilities/ptz.d.ts +289 -0
- package/dist/model/capabilities/rtsp.d.ts +221 -0
- package/dist/model/capabilities/siren.d.ts +218 -0
- package/dist/model/capabilities/smart-light.d.ts +172 -0
- package/dist/model/capabilities/smoke.d.ts +40 -0
- package/dist/model/capabilities/snapshot.d.ts +6 -0
- package/dist/model/capabilities/storage.d.ts +11 -0
- package/dist/model/capabilities/suction.d.ts +104 -0
- package/dist/model/capabilities/types.d.ts +484 -0
- package/dist/model/capabilities/vacuum-clean.d.ts +1946 -0
- package/dist/model/capabilities/vacuum-dock.d.ts +208 -0
- package/dist/model/capabilities/video.d.ts +6 -0
- package/dist/model/classify.d.ts +78 -0
- package/dist/model/clean-record-detail.d.ts +65 -0
- package/dist/model/clean-records.d.ts +69 -0
- package/dist/model/device-family.d.ts +73 -0
- package/dist/model/device-types.d.ts +123 -0
- package/dist/model/device.d.ts +265 -0
- package/dist/model/index.d.ts +29 -4
- package/dist/model/infer.d.ts +23 -0
- package/dist/model/inspect.d.ts +61 -0
- package/dist/model/life-params.d.ts +21 -0
- package/dist/model/map-pixels.d.ts +70 -0
- package/dist/model/param-dictionary.d.ts +28 -0
- package/dist/model/param-namespace.d.ts +21 -0
- package/dist/model/proto-read.d.ts +53 -0
- package/dist/model/push-events.d.ts +147 -0
- package/dist/model/registry.d.ts +54 -0
- package/dist/model/types.d.ts +301 -0
- package/dist/model/vacuum-map-store.d.ts +92 -0
- package/dist/model/vacuum-map.d.ts +286 -0
- package/dist/model/vacuum-scenes.d.ts +76 -0
- package/dist/model/vacuum-schedules.d.ts +85 -0
- package/dist/transport/dp-preset.d.ts +102 -0
- package/dist/transport/ff09.d.ts +444 -0
- package/dist/transport/ffmpeg.d.ts +86 -0
- package/dist/transport/http/decodeImageV1.d.ts +20 -0
- package/dist/transport/http/decodeImageV2.d.ts +19 -0
- package/dist/transport/http/index.d.ts +5 -0
- package/dist/transport/http/light-catalog.d.ts +62 -0
- package/dist/transport/http/media-download.d.ts +14 -0
- package/dist/transport/http/mega-client.d.ts +514 -0
- package/dist/transport/http/phone-model.d.ts +21 -0
- package/dist/transport/index.d.ts +10 -7
- package/dist/transport/mqtt/app-client-id.d.ts +16 -0
- package/dist/transport/mqtt/availability.d.ts +14 -0
- package/dist/transport/mqtt/bare-ip-tls.d.ts +46 -0
- package/dist/transport/mqtt/biz-stream.d.ts +98 -0
- package/dist/transport/mqtt/broker-discovery.d.ts +55 -0
- package/dist/transport/mqtt/clean-codec.d.ts +14 -0
- package/dist/transport/mqtt/command-router.d.ts +285 -0
- package/dist/transport/mqtt/dp-codec.d.ts +58 -0
- package/dist/transport/mqtt/dp-color.d.ts +14 -0
- package/dist/transport/mqtt/engine.d.ts +16 -0
- package/dist/transport/mqtt/index.d.ts +5 -0
- package/dist/transport/mqtt/secure-mqtt.d.ts +107 -0
- package/dist/transport/mqtt/topics.d.ts +80 -0
- package/dist/transport/p2p/adts.d.ts +91 -0
- package/dist/transport/p2p/annexb.d.ts +124 -0
- package/dist/transport/p2p/codec.d.ts +166 -0
- package/dist/transport/p2p/command-router.d.ts +655 -0
- package/dist/transport/p2p/commands.d.ts +550 -0
- package/dist/transport/p2p/envelope.d.ts +46 -0
- package/dist/transport/p2p/fmp4.d.ts +89 -0
- package/dist/transport/p2p/fragment-recording.d.ts +33 -0
- package/dist/transport/p2p/index.d.ts +13 -0
- package/dist/transport/p2p/lan-ip.d.ts +25 -0
- package/dist/transport/p2p/live-stream.d.ts +223 -0
- package/dist/transport/p2p/live-trace.d.ts +123 -0
- package/dist/transport/p2p/media.d.ts +105 -0
- package/dist/transport/p2p/p2p-session.d.ts +620 -0
- package/dist/transport/p2p/readable-egress.d.ts +27 -0
- package/dist/transport/p2p/session-manager.d.ts +154 -0
- package/dist/transport/p2p/shared-live-source.d.ts +431 -0
- package/dist/transport/p2p/talkback.d.ts +187 -0
- package/dist/transport/p2p/video.d.ts +150 -0
- package/dist/transport/p2p/write-commands.d.ts +21 -0
- package/dist/transport/protobuf.d.ts +5 -0
- package/dist/transport/push/fcm.d.ts +23 -0
- package/dist/transport/push/index.d.ts +6 -0
- package/dist/transport/push/message-tags.d.ts +26 -0
- package/dist/transport/push/parser.d.ts +27 -0
- package/dist/transport/push/proto.d.ts +11 -0
- package/dist/transport/push/push-client.d.ts +64 -0
- package/dist/transport/push/store.d.ts +23 -0
- package/dist/transport/push/types.d.ts +180 -0
- package/dist/transport/raw-dp.d.ts +6 -0
- package/dist/transport/stored-image-cache.d.ts +23 -0
- package/dist/transport/tuya/account.d.ts +44 -0
- package/dist/transport/tuya/client.d.ts +89 -0
- package/dist/transport/tuya/command-router.d.ts +79 -0
- package/dist/transport/tuya/dp-codec.d.ts +56 -0
- package/dist/transport/tuya/index.d.ts +28 -0
- package/dist/transport/tuya/request.d.ts +157 -0
- package/dist/transport/tuya/sign.d.ts +64 -0
- package/package.json +12 -13
- package/dist/client/index.js +0 -2
- package/dist/client/index.js.map +0 -1
- package/dist/core/index.js +0 -2
- package/dist/core/index.js.map +0 -1
- package/dist/model/index.js +0 -2
- package/dist/model/index.js.map +0 -1
- package/dist/transport/index.js +0 -2
- package/dist/transport/index.js.map +0 -1
|
@@ -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
|
+
}
|