@mega-yfue/eufy-sdk 0.6.0-beta.2 → 0.6.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/client/device-registry.d.ts +6 -0
  2. package/dist/client/eufy-mega.d.ts +84 -11
  3. package/dist/client/types.d.ts +12 -9
  4. package/dist/core/contracts.d.ts +3 -1
  5. package/dist/core/store.d.ts +12 -1
  6. package/dist/core/types.d.ts +4 -2
  7. package/dist/index.js +2719 -2120
  8. package/dist/index.js.map +4 -4
  9. package/dist/model/capabilities/camera.d.ts +4 -1
  10. package/dist/model/capabilities/mower.d.ts +3 -2
  11. package/dist/model/capabilities/vacuum-clean.d.ts +23 -0
  12. package/dist/model/device-family.d.ts +1 -1
  13. package/dist/model/device-types.d.ts +1 -0
  14. package/dist/transport/http/mega-client.d.ts +3 -2
  15. package/dist/transport/mqtt/biz-stream.d.ts +22 -25
  16. package/dist/transport/p2p/audio-frame.d.ts +13 -0
  17. package/dist/transport/p2p/codec.d.ts +2 -2
  18. package/dist/transport/p2p/command-router.d.ts +28 -3
  19. package/dist/transport/p2p/database-wire.d.ts +30 -0
  20. package/dist/transport/p2p/level2-settle.d.ts +9 -0
  21. package/dist/transport/p2p/live-stream.d.ts +34 -7
  22. package/dist/transport/p2p/live-trace.d.ts +6 -4
  23. package/dist/transport/p2p/p2p-session.d.ts +99 -15
  24. package/dist/transport/p2p/readable-egress.d.ts +3 -1
  25. package/dist/transport/p2p/recording-download.d.ts +26 -4
  26. package/dist/transport/p2p/shared-live-source.d.ts +9 -3
  27. package/dist/transport/p2p/video.d.ts +4 -2
  28. package/dist/transport/push/store.d.ts +7 -6
  29. package/dist/transport/rtc/command-router.d.ts +63 -3
  30. package/dist/transport/rtc/portal-packet.d.ts +3 -13
  31. package/dist/transport/rtc/session.d.ts +4 -2
  32. package/dist/transport/stored-image-cache.d.ts +14 -0
  33. package/package.json +1 -1
@@ -248,6 +248,12 @@ export declare class DeviceRegistry {
248
248
  * for an expired token is the same lie {@link getDevices} stopped telling, one level down.
249
249
  */
250
250
  refreshedList(sn: string): Promise<EufyDevice | undefined>;
251
+ /**
252
+ * Record that `sn` was heard on a realtime wire at `atMs`, advancing its {@link EufyDevice.lastSeenMs}.
253
+ *
254
+ * Only ever moves the time forward, and survives a {@link getDevices} rebuild.
255
+ */
256
+ markSeen(sn: string, atMs: number): void;
251
257
  /**
252
258
  * Record state a device reported over its realtime wire, merging into whatever it last reported.
253
259
  *
@@ -20,8 +20,8 @@ import { type P2PSession } from "../transport/p2p/p2p-session.js";
20
20
  import { type CleanRecordPage } from "../model/clean-records.js";
21
21
  import { type AvailabilityObservation, type EufyDevice } from "../core/types.js";
22
22
  import { Device, type DeviceInspection } from "../model/index.js";
23
- import type { EufyMegaOptions, EufyMegaEvent, EufyMegaEventMap, DeviceState, RealtimeReadiness, StationFace, StationFacesOptions, WaitForRealtimeOptions } from "./types.js";
24
- export type { EufyMegaOptions, EufyMegaEvent, EufyMegaEventMap, AnyDeviceEvent, DeviceState, RealtimePlaneReadiness, RealtimeReadiness, StationFace, StationFacesOptions, WaitForRealtimeOptions, } from "./types.js";
23
+ import type { EufyMegaOptions, EufyMegaEvent, EufyMegaEventMap, DeviceState, RealtimeReadiness, StationFace, StationReadOptions, WaitForRealtimeOptions } from "./types.js";
24
+ export type { EufyMegaOptions, EufyMegaEvent, EufyMegaEventMap, AnyDeviceEvent, DeviceState, RealtimePlaneReadiness, RealtimeReadiness, StationFace, StationReadOptions, WaitForRealtimeOptions, } from "./types.js";
25
25
  export interface EufyMega {
26
26
  on<E extends EufyMegaEvent>(event: E, listener: (...args: EufyMegaEventMap[E]) => void): this;
27
27
  once<E extends EufyMegaEvent>(event: E, listener: (...args: EufyMegaEventMap[E]) => void): this;
@@ -54,6 +54,12 @@ export declare class EufyMega extends EventEmitter {
54
54
  /** Shared in-flight/settled `startMqtt()` promise PER SCOPE — so realtime bring-up and an on-demand
55
55
  * publish await the SAME fully-connected transport instead of racing (and never open two). */
56
56
  private readonly mqttReady;
57
+ /**
58
+ * Serials {@link subscribeMqttDevices} has subscribed or is subscribing, per transport, so a later pass
59
+ * subscribes only devices that transport has not carried yet. Keyed by transport, so a new connection
60
+ * starts empty.
61
+ */
62
+ private readonly mqttSubscribed;
57
63
  /** Device list/record/capability resolution + the frame→caps cache. */
58
64
  private readonly registry;
59
65
  private pushClient?;
@@ -123,6 +129,12 @@ export declare class EufyMega extends EventEmitter {
123
129
  private readonly pollTimer;
124
130
  /** Armed while a failed push bring-up waits to be retried; cancelled by {@link disconnect}. */
125
131
  private readonly pushRetryTimer;
132
+ /**
133
+ * Per wired station, the pending reopen of its session and the pause before the next one; see
134
+ * {@link reopenWiredStation}. Dropped for a station when its session connects, and wholly by
135
+ * {@link disconnect}.
136
+ */
137
+ private readonly stationReopens;
126
138
  constructor(opts: EufyMegaOptions);
127
139
  /**
128
140
  * Builds the inbound listener that converts ThingClips DP maps to realtime capability state.
@@ -307,7 +319,16 @@ export declare class EufyMega extends EventEmitter {
307
319
  * manager's own log, since by then nothing is waiting to receive it.
308
320
  */
309
321
  private recycleStandaloneSession;
310
- /** Reopen a wired station that {@link recycleStandaloneSession} closed, while the generation that warmed it is current. */
322
+ /**
323
+ * Reopen a wired station's session while the generation that warmed it is current: after
324
+ * {@link recycleStandaloneSession} closed it, after it closed on its own, or after its warm-up failed.
325
+ *
326
+ * A wired station's session is the persistent one realtime start opened, so a station left without one
327
+ * receives nothing until a command dials it again. The first reopen runs at once; one that fails is
328
+ * retried after {@link STATION_REOPEN_MIN_MS}, doubling up to {@link STATION_REOPEN_MAX_MS}, until the
329
+ * session connects or the generation ends. One reopen is pending per station at a time, and each failure
330
+ * is reported once, by {@link P2PCommandRouter.ensureStation}.
331
+ */
311
332
  private reopenWiredStation;
312
333
  /**
313
334
  * Whether the DECODED property now reads what the write asked for.
@@ -333,6 +354,8 @@ export declare class EufyMega extends EventEmitter {
333
354
  * fans same-kind sensors out by channel, so the resolved serial is folded in; a payload's own
334
355
  * fields still win on conflict.
335
356
  *
357
+ * A station's camera-info table lands per channel, each channel's params on the device attached there.
358
+ *
336
359
  * Params the station volunteered land as device state first. They arrive in the cloud record's own
337
360
  * `param_type → value` shape, so no capability has to claim an id it does not own, and the reads
338
361
  * they back stop waiting for the next cloud poll. A capability decode follows for the frames that
@@ -385,7 +408,9 @@ export declare class EufyMega extends EventEmitter {
385
408
  /**
386
409
  * On a successful login, kick off auto-realtime (unless `autoRealtime:false`). Fire-and-forget so
387
410
  * `login()` returns as soon as the session is ready — realtime channels come up in the background and
388
- * surface failures via `error`. Idempotent through the retained generation promise.
411
+ * surface failures via `error`. Idempotent through the retained generation promise. A login for a
412
+ * different account drops every stored image; one for the same account drops only the lifecycle
413
+ * failures recorded against the session it replaces.
389
414
  */
390
415
  private afterLogin;
391
416
  /**
@@ -458,7 +483,8 @@ export declare class EufyMega extends EventEmitter {
458
483
  private rtcRouteFor;
459
484
  /**
460
485
  * The RTC media provider for a station on a media-track route, or a camera attached to a T9000 whose
461
- * `device_channel` resolves back to it on the station; `undefined` otherwise, unless an NVR camera has its own dedicated media call.
486
+ * `device_channel` resolves back to it on the station; `undefined` otherwise. A camera attached to a T8N00 gets the
487
+ * NVR's dedicated media call when {@link isNvrMediaCamera} admits it.
462
488
  */
463
489
  private rtcMediaFor;
464
490
  /**
@@ -534,6 +560,8 @@ export declare class EufyMega extends EventEmitter {
534
560
  * first announces it and the others stay silent. Its timing says only when a caller happened to read; the
535
561
  * value is the news. It applies what the device volunteered over realtime on top of the cloud half, which
536
562
  * the registry keeps apart, so it can neither revert nor announce a revert of a report already landed.
563
+ * Nothing awaits that refresh, so its failure is reported like the other background paths' — a dead
564
+ * token as `sessionExpired`, anything else as `error` — and the read keeps its last-known value.
537
565
  *
538
566
  * The `Device` returned is held WEAKLY: it is what the inbound paths announce against, so a caller that
539
567
  * wants property changes for a serial keeps its own reference. Dropping it stops the announcements, not
@@ -624,6 +652,12 @@ export declare class EufyMega extends EventEmitter {
624
652
  * device absent from the previous pass is skipped — first sight is discovery, not a transition;
625
653
  * {@link deviceState} answers an initial reading.
626
654
  *
655
+ * A device with changed params is re-resolved by {@link widenCapabilities} once per pass, however many
656
+ * of its params moved, since each re-resolution is a cloud param-list read.
657
+ *
658
+ * Then brings the secure-MQTT plane up to the roster ({@link syncMqttRoster}): the startup bring-up
659
+ * sized it from the roster at login, which is empty when the cloud was unreachable then.
660
+ *
627
661
  * Then re-reads every Tuya clean-line robot from the Tuya cloud ({@link readTuyaDevices}): its state
628
662
  * is not in the cloud record this pass diffs and no live channel carries it, so the poll is where it
629
663
  * stays fresh. That read runs after the `try`, not inside it, so a cloud error on the list diff does
@@ -737,7 +771,8 @@ export declare class EufyMega extends EventEmitter {
737
771
  */
738
772
  keepStationWarm(stationSn: string): () => void;
739
773
  /** Eagerly open P2P sessions for WIRED stations only (persistent — they don't drain). Battery
740
- * stations stay closed until an on-demand open. Best-effort per station. */
774
+ * stations stay closed until an on-demand open. Best-effort per station: one that fails is handed to
775
+ * {@link reopenWiredStation}. */
741
776
  private warmWiredP2P;
742
777
  /**
743
778
  * A station's power tier for the P2P lifecycle: a HomeBase/station is `"wired"` (persistent); a
@@ -810,11 +845,20 @@ export declare class EufyMega extends EventEmitter {
810
845
  /**
811
846
  * Subscribe the devices on one credential scope; a failing subscribe is reported, not fatal (one
812
847
  * unreachable device must not stop the rest of the roster from coming up).
848
+ * A device is recorded in that transport's {@link mqttSubscribed} entry before its subscribe is awaited,
849
+ * whatever the outcome, so a pass running alongside or after skips it: each device is attempted once
850
+ * per transport.
813
851
  *
814
852
  * A Tuya clean-line device (`eufy_home_tuya`) is not subscribed: its state lives on the Tuya cloud,
815
853
  * and the broker refuses its `eufy_home` topics on every credential.
816
854
  */
817
855
  private subscribeMqttDevices;
856
+ /**
857
+ * Start every credential scope the roster now needs and has no transport for, and subscribe the
858
+ * devices that joined a scope already up — each scope's failure reported, not thrown. A no-op once
859
+ * the realtime generation is superseded or was never started.
860
+ */
861
+ private syncMqttRoster;
818
862
  /**
819
863
  * Send whatever a device's capabilities want sent once its realtime channel is up — for a line whose
820
864
  * state is pushed on change with no heartbeat, the request that makes its state readable before the
@@ -835,19 +879,45 @@ export declare class EufyMega extends EventEmitter {
835
879
  */
836
880
  getP2pSessions(): Map<string, P2PSession>;
837
881
  /**
838
- * The people enrolled on a station, read from its own `person_basic_info` table over P2P.
882
+ * The people enrolled on a station, read from its own `person_basic_info` table through
883
+ * {@link readStationTable}.
839
884
  *
840
885
  * The cloud roster (`api.getFaces()`) answers empty for an account that keeps its faces on the
841
886
  * station, because they were never uploaded — the station holds them.
842
887
  *
843
- * Resolves with the rows the station returned. Rejects on a station that cannot be reached, on a
844
- * record stating no `admin_user_id`, and on the reply not arriving whole in time. Assembling that
845
- * reply is `P2PSession.readDatabase`'s; this resolves which station and which account id.
888
+ * Resolves with the rows the station returned that are objects. Rejects as {@link readStationTable}
889
+ * does.
846
890
  *
847
891
  * `stranger<n>` names are the station's own placeholders for a face nobody has named, and are
848
892
  * returned as they arrive; a caller that wants only named people filters them.
849
893
  */
850
- getStationFaces(stationSn: string, opts?: StationFacesOptions): Promise<StationFace[]>;
894
+ getStationFaces(stationSn: string, opts?: StationReadOptions): Promise<StationFace[]>;
895
+ /**
896
+ * The rows of one of a station's own tables (`history_record_info`, `person_basic_info`, …), read over
897
+ * the transport the station speaks: the control channel of a HomeBase S1 Pro (T9000), P2P for any
898
+ * other station. The query is the table's whole content; the rows are the station's, shaped by it.
899
+ *
900
+ * Resolves with the rows the station returned. Rejects on a serial not in the device list, on a record
901
+ * stating no `admin_user_id`, on a station that cannot be reached, on a query the station refuses, and
902
+ * on the reply not arriving in time. Assembling the reply is the transport's; this resolves which
903
+ * station, which transport and which account id.
904
+ */
905
+ readStationTable(stationSn: string, table: string, opts?: StationReadOptions): Promise<unknown[]>;
906
+ /**
907
+ * One image stored on a HomeBase S1 Pro (T9000), by the on-station path a table row names
908
+ * (`thumb_path`, …), read over the station's control channel.
909
+ *
910
+ * Resolves with the image bytes as the station holds them. Rejects on a serial not in the device list,
911
+ * on a record stating no `admin_user_id`, on a station other than a T9000, on a station that cannot be
912
+ * reached, and on the reply not arriving in time. Fetching the image is the transport's; this resolves
913
+ * which station and which account id.
914
+ */
915
+ readStationImage(stationSn: string, filePath: string, opts?: StationReadOptions): Promise<Buffer>;
916
+ /**
917
+ * The station a database read goes to, its `admin_user_id` and, for a T9000, its RTC route. Throws,
918
+ * prefixed with `op`, on a serial the device list does not carry and on a record stating no admin id.
919
+ */
920
+ private stationReadTarget;
851
921
  /** The station's own `admin_user_id`, which its database refuses the query without. */
852
922
  private adminUserIdOf;
853
923
  /**
@@ -959,6 +1029,9 @@ export declare class EufyMega extends EventEmitter {
959
1029
  * Returns the connected client rather than installing it. Registration can outlive a `disconnect()`,
960
1030
  * and {@link ensureRealtime} owns the decision of whether a finished bring-up is still the current
961
1031
  * one — so this never overwrites a channel a later login already brought up.
1032
+ *
1033
+ * Persisting the seen ids after each push reports a failed `save` through `error` rather than throwing:
1034
+ * the push handler runs inside the MCS socket's `data` callback, where a throw is an uncaught exception.
962
1035
  */
963
1036
  private startPush;
964
1037
  /**
@@ -202,9 +202,11 @@ export interface EufyMegaOptions extends MegaClientConfig {
202
202
  * that choice on every host. The SDK reports when the device last spoke; the caller decides what that
203
203
  * means — the same split as the snapshot cache TTL and the live power budget.
204
204
  *
205
- * P2P session state is **not** a liveness signal and is not carried here: sessions are opened only
206
- * when something needs one and closed when idle, so "no session" is the resting state of a healthy
207
- * device. Transport visibility lives on `getP2pSessions()` and the `p2pConnect`/`p2pClose` events,
205
+ * The absence of a P2P session is **not** a liveness signal and is not carried here: sessions are opened
206
+ * only when something needs one and closed when idle, so "no session" is the resting state of a healthy
207
+ * device. A station answering an open session is the opposite fact, positive evidence, and does advance
208
+ * the station's own {@link DeviceState.lastSeenMs} — never that of a device it covers, whose liveness its answer does
209
+ * not show. Transport visibility lives on `getP2pSessions()` and the `p2pConnect`/`p2pClose` events,
208
210
  * station-scoped like the session itself.
209
211
  *
210
212
  * The cloud record carries no connectivity field either: it has no `status` / `device_online`, and the
@@ -215,8 +217,9 @@ export interface DeviceState {
215
217
  /** The parent station whose P2P session covers this device (itself, when standalone). */
216
218
  stationSn: string;
217
219
  /**
218
- * When the device last reported to the cloud, in ms. Bounded by
219
- * the cloud's own slow refresh — minutes, not seconds — so it answers "is this device alive at all",
220
+ * When the device was last heard from, in ms: its last report to the cloud, or a later one over a
221
+ * realtime wire, a station's heartbeat answer on its open P2P session included. The cloud half is bounded
222
+ * by the cloud's own slow refresh — minutes, not seconds — so it answers "is this device alive at all",
220
223
  * not "what is it doing right now".
221
224
  */
222
225
  lastSeenMs?: number;
@@ -240,9 +243,9 @@ export interface StationFace {
240
243
  group_id?: number | string;
241
244
  [column: string]: unknown;
242
245
  }
243
- /** How long to wait for a station's database reply, and how to give up early. */
244
- export interface StationFacesOptions {
245
- /** Default 15s. A full table is a handful of frames; a station that will not answer says nothing. */
246
+ /** How long to wait for a station's reply to a read, and how to give up early. */
247
+ export interface StationReadOptions {
248
+ /** Default 15s. A station that will not answer says nothing. */
246
249
  timeoutMs?: number;
247
250
  signal?: AbortSignal;
248
251
  }
@@ -332,7 +335,7 @@ export type EufyMegaEventMap = {
332
335
  map: VacuumMapSnapshot;
333
336
  }];
334
337
  /**
335
- * A device reported to the cloud since the last poll — its {@link DeviceState.lastSeenMs} advanced.
338
+ * A device was heard from since the last poll — its {@link DeviceState.lastSeenMs} advanced.
336
339
  * Carries {@link DeviceState}; the host applies its own staleness threshold.
337
340
  *
338
341
  * Transport/session lifecycle is NOT this event: that's `p2pConnect`/`p2pClose`, station-scoped where
@@ -852,7 +852,9 @@ export interface MediaProvider {
852
852
  * {@link ReceivedVideoFrame} envelopes whose original source-arrival time survives retained replay.
853
853
  * Raw mode ignores arrivalTime. Audio is available separately through {@link live} or muxed through
854
854
  * {@link recordFragments}; it is never interleaved into raw video. The caller owns the Readable's
855
- * lifetime, and destroying it releases the shared pull.
855
+ * lifetime, and destroying it releases the shared pull. On a battery-budgeted source the Readable
856
+ * emits a `budget` event with a {@link StreamBudgetNotice}; unless `extend()` is called within its
857
+ * grace, the Readable ends.
856
858
  */
857
859
  openReadable?(opts?: {
858
860
  objectMode?: boolean;
@@ -48,7 +48,18 @@ export declare class MemorySessionStore<T = PersistedSession> implements Session
48
48
  save(s: T): void;
49
49
  clear(): void;
50
50
  }
51
- /** JSON-file store, e.g. new FileSessionStore("./.eufy-session.json"). */
51
+ /**
52
+ * JSON-file store, e.g. new FileSessionStore("./.eufy-session.json").
53
+ *
54
+ * `save` resolves a symlinked path to its real file (the file a dangling symlink names, created with its
55
+ * directory), writes a fresh owner-only (`0o600`) temp file under a random name beside that file and
56
+ * renames it over it, so where the directory allows it the file on disk is always a complete record — the
57
+ * old one until the rename, the new one after — and the symlink itself stays in place. When the directory
58
+ * refuses the temp file or the rename (`EACCES`, `EPERM`, `EBUSY`, `EXDEV`, `EROFS`: a read-only directory
59
+ * around a writable file, a single-file bind mount), `save` rewrites the file in place instead, which is
60
+ * not atomic and keeps an existing file's mode. The atomic path always leaves `0o600`, whatever mode an
61
+ * existing file had. Any other failure removes the temp file and throws.
62
+ */
52
63
  export declare class FileSessionStore<T = PersistedSession> implements SessionStore<T> {
53
64
  private readonly path;
54
65
  constructor(path: string);
@@ -80,8 +80,10 @@ export interface EufyDevice {
80
80
  */
81
81
  paramUpdatedAt?: Record<number, number>;
82
82
  /**
83
- * The most recent {@link paramUpdatedAt} across all of this device's params, in **milliseconds**
84
- * (comparable to `Date.now()` directly). `undefined` when the record carried no timestamp at all.
83
+ * When the device was last heard from, in **milliseconds** (comparable to `Date.now()` directly): the
84
+ * most recent {@link paramUpdatedAt} across all of its params, or a later report over a realtime wire,
85
+ * including a station answering its own P2P session's heartbeat. Never moves backwards. `undefined` when
86
+ * neither has been seen.
85
87
  *
86
88
  * This is an observation — "the device last reported at T" — deliberately **not** a reachability
87
89
  * verdict. How long a silence means "unreachable" depends on the device (a mains camera heartbeats