@mega-yfue/eufy-sdk 0.6.0-beta.14 → 0.6.0-beta.16

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.
@@ -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?;
@@ -385,7 +391,9 @@ export declare class EufyMega extends EventEmitter {
385
391
  /**
386
392
  * On a successful login, kick off auto-realtime (unless `autoRealtime:false`). Fire-and-forget so
387
393
  * `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.
394
+ * surface failures via `error`. Idempotent through the retained generation promise. A login for a
395
+ * different account drops every stored image; one for the same account drops only the lifecycle
396
+ * failures recorded against the session it replaces.
389
397
  */
390
398
  private afterLogin;
391
399
  /**
@@ -627,6 +635,12 @@ export declare class EufyMega extends EventEmitter {
627
635
  * device absent from the previous pass is skipped — first sight is discovery, not a transition;
628
636
  * {@link deviceState} answers an initial reading.
629
637
  *
638
+ * A device with changed params is re-resolved by {@link widenCapabilities} once per pass, however many
639
+ * of its params moved, since each re-resolution is a cloud param-list read.
640
+ *
641
+ * Then brings the secure-MQTT plane up to the roster ({@link syncMqttRoster}): the startup bring-up
642
+ * sized it from the roster at login, which is empty when the cloud was unreachable then.
643
+ *
630
644
  * Then re-reads every Tuya clean-line robot from the Tuya cloud ({@link readTuyaDevices}): its state
631
645
  * is not in the cloud record this pass diffs and no live channel carries it, so the poll is where it
632
646
  * stays fresh. That read runs after the `try`, not inside it, so a cloud error on the list diff does
@@ -813,11 +827,20 @@ export declare class EufyMega extends EventEmitter {
813
827
  /**
814
828
  * Subscribe the devices on one credential scope; a failing subscribe is reported, not fatal (one
815
829
  * unreachable device must not stop the rest of the roster from coming up).
830
+ * A device is recorded in that transport's {@link mqttSubscribed} entry before its subscribe is awaited,
831
+ * whatever the outcome, so a pass running alongside or after skips it: each device is attempted once
832
+ * per transport.
816
833
  *
817
834
  * A Tuya clean-line device (`eufy_home_tuya`) is not subscribed: its state lives on the Tuya cloud,
818
835
  * and the broker refuses its `eufy_home` topics on every credential.
819
836
  */
820
837
  private subscribeMqttDevices;
838
+ /**
839
+ * Start every credential scope the roster now needs and has no transport for, and subscribe the
840
+ * devices that joined a scope already up — each scope's failure reported, not thrown. A no-op once
841
+ * the realtime generation is superseded or was never started.
842
+ */
843
+ private syncMqttRoster;
821
844
  /**
822
845
  * Send whatever a device's capabilities want sent once its realtime channel is up — for a line whose
823
846
  * state is pushed on change with no heartbeat, the request that makes its state readable before the
@@ -838,19 +861,45 @@ export declare class EufyMega extends EventEmitter {
838
861
  */
839
862
  getP2pSessions(): Map<string, P2PSession>;
840
863
  /**
841
- * The people enrolled on a station, read from its own `person_basic_info` table over P2P.
864
+ * The people enrolled on a station, read from its own `person_basic_info` table through
865
+ * {@link readStationTable}.
842
866
  *
843
867
  * The cloud roster (`api.getFaces()`) answers empty for an account that keeps its faces on the
844
868
  * station, because they were never uploaded — the station holds them.
845
869
  *
846
- * Resolves with the rows the station returned. Rejects on a station that cannot be reached, on a
847
- * record stating no `admin_user_id`, and on the reply not arriving whole in time. Assembling that
848
- * reply is `P2PSession.readDatabase`'s; this resolves which station and which account id.
870
+ * Resolves with the rows the station returned that are objects. Rejects as {@link readStationTable}
871
+ * does.
849
872
  *
850
873
  * `stranger<n>` names are the station's own placeholders for a face nobody has named, and are
851
874
  * returned as they arrive; a caller that wants only named people filters them.
852
875
  */
853
- getStationFaces(stationSn: string, opts?: StationFacesOptions): Promise<StationFace[]>;
876
+ getStationFaces(stationSn: string, opts?: StationReadOptions): Promise<StationFace[]>;
877
+ /**
878
+ * The rows of one of a station's own tables (`history_record_info`, `person_basic_info`, …), read over
879
+ * the transport the station speaks: the control channel of a HomeBase S1 Pro (T9000), P2P for any
880
+ * other station. The query is the table's whole content; the rows are the station's, shaped by it.
881
+ *
882
+ * Resolves with the rows the station returned. Rejects on a serial not in the device list, on a record
883
+ * stating no `admin_user_id`, on a station that cannot be reached, on a query the station refuses, and
884
+ * on the reply not arriving in time. Assembling the reply is the transport's; this resolves which
885
+ * station, which transport and which account id.
886
+ */
887
+ readStationTable(stationSn: string, table: string, opts?: StationReadOptions): Promise<unknown[]>;
888
+ /**
889
+ * One image stored on a HomeBase S1 Pro (T9000), by the on-station path a table row names
890
+ * (`thumb_path`, …), read over the station's control channel.
891
+ *
892
+ * Resolves with the image bytes as the station holds them. Rejects on a serial not in the device list,
893
+ * on a record stating no `admin_user_id`, on a station other than a T9000, on a station that cannot be
894
+ * reached, and on the reply not arriving in time. Fetching the image is the transport's; this resolves
895
+ * which station and which account id.
896
+ */
897
+ readStationImage(stationSn: string, filePath: string, opts?: StationReadOptions): Promise<Buffer>;
898
+ /**
899
+ * The station a database read goes to, its `admin_user_id` and, for a T9000, its RTC route. Throws,
900
+ * prefixed with `op`, on a serial the device list does not carry and on a record stating no admin id.
901
+ */
902
+ private stationReadTarget;
854
903
  /** The station's own `admin_user_id`, which its database refuses the query without. */
855
904
  private adminUserIdOf;
856
905
  /**
@@ -962,6 +1011,9 @@ export declare class EufyMega extends EventEmitter {
962
1011
  * Returns the connected client rather than installing it. Registration can outlive a `disconnect()`,
963
1012
  * and {@link ensureRealtime} owns the decision of whether a finished bring-up is still the current
964
1013
  * one — so this never overwrites a channel a later login already brought up.
1014
+ *
1015
+ * Persisting the seen ids after each push reports a failed `save` through `error` rather than throwing:
1016
+ * the push handler runs inside the MCS socket's `data` callback, where a throw is an uncaught exception.
965
1017
  */
966
1018
  private startPush;
967
1019
  /**
@@ -240,9 +240,9 @@ export interface StationFace {
240
240
  group_id?: number | string;
241
241
  [column: string]: unknown;
242
242
  }
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. */
243
+ /** How long to wait for a station's reply to a read, and how to give up early. */
244
+ export interface StationReadOptions {
245
+ /** Default 15s. A station that will not answer says nothing. */
246
246
  timeoutMs?: number;
247
247
  signal?: AbortSignal;
248
248
  }
@@ -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);