@mega-yfue/eufy-sdk 0.5.0-beta.77 → 0.5.0-beta.79

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.
@@ -734,6 +734,10 @@ export interface LiveStreamConsumer extends LiveStreamHandle {
734
734
  export interface AbortableCall {
735
735
  signal?: AbortSignal;
736
736
  }
737
+ /** Select an optical view; absence selects view 0. */
738
+ export interface MediaSourceSelection {
739
+ view?: number;
740
+ }
737
741
  export interface SharedSourceHints {
738
742
  /**
739
743
  * Power source, a runtime device fact (`"battery"` incl. solar, or `"wired"`) — never a device-family
@@ -759,6 +763,8 @@ export interface SharedSourceHints {
759
763
  * because an unbound model has no live client (hence `?.` at the call site).
760
764
  */
761
765
  export interface MediaProvider {
766
+ /** Optical view identifiers supported by this bound media route. */
767
+ views?: readonly number[];
762
768
  /**
763
769
  * Return the latest validated push thumbnail retained in memory. This passive operation performs no
764
770
  * network, storage, P2P, live-media, or transcoding work at call time. It rejects with
@@ -785,7 +791,7 @@ export interface MediaProvider {
785
791
  timeoutMs?: number;
786
792
  collectMs?: number;
787
793
  skipKeyframes?: number;
788
- } & SharedSourceHints & AbortableCall): Promise<{
794
+ } & SharedSourceHints & AbortableCall & MediaSourceSelection): Promise<{
789
795
  jpeg: Buffer;
790
796
  width: number;
791
797
  height: number;
@@ -817,7 +823,7 @@ export interface MediaProvider {
817
823
  * stream?.stop(); // detach this consumer
818
824
  * ```
819
825
  */
820
- live(opts?: SharedSourceHints & AbortableCall & Record<string, unknown>): Promise<LiveStreamConsumer>;
826
+ live(opts?: SharedSourceHints & AbortableCall & MediaSourceSelection & Record<string, unknown>): Promise<LiveStreamConsumer>;
821
827
  /**
822
828
  * Record `seconds` of video → an mp4/h264 buffer.
823
829
  *
@@ -843,7 +849,7 @@ export interface MediaProvider {
843
849
  openReadable?(opts?: {
844
850
  objectMode?: boolean;
845
851
  arrivalTime?: boolean;
846
- } & SharedSourceHints & AbortableCall): Promise<import("node:stream").Readable>;
852
+ } & SharedSourceHints & AbortableCall & MediaSourceSelection): Promise<import("node:stream").Readable>;
847
853
  /**
848
854
  * Continuously record the live feed as fragmented-MP4 (CMAF). The caller-owned
849
855
  * {@link FragmentRecordingHandle} yields an init segment then keyframe-bounded media fragments,
package/dist/index.js CHANGED
@@ -3656,6 +3656,7 @@ var isHomeBase = (ctx) => has(HOMEBASE_TYPES, ctx.deviceType);
3656
3656
  var isStation9000 = (ctx) => ctx.deviceType === DeviceType.STATION_9000;
3657
3657
  var isNvrT8N00 = (ctx) => ctx.model === "T8N00";
3658
3658
  var isNvrMediaCamera = (ctx) => ctx.model === "T8E00" || ctx.model === "T8P00";
3659
+ var isNvrDualLensCamera = (ctx) => ctx.model === "T8E00";
3659
3660
  var isDoorbellT8223 = (ctx) => ctx.model === "T8223";
3660
3661
  var isAiotVacuum = (ctx) => ctx.category !== "eufy_home_tuya";
3661
3662
  var isTuyaVacuum = (ctx) => ctx.category === "eufy_home_tuya";
@@ -5591,6 +5592,11 @@ function refuseWhenDisabled(ctx, read) {
5591
5592
  if (CAMERA_MEMBERS.enabled.readReflectsWrite(ctx) && read("enabled")?.value === false)
5592
5593
  throw new CameraDisabledError(ctx.name ?? ctx.serial);
5593
5594
  }
5595
+ function refuseUnsupportedView(media, opts) {
5596
+ const view = opts?.view ?? 0;
5597
+ if (!(media.views ?? [0]).includes(view))
5598
+ throw new RangeError(`camera: unsupported optical view ${view}`);
5599
+ }
5594
5600
  var CAMERA_MEMBERS = {
5595
5601
  /** Reads the latest cached report and exposes no setter. */
5596
5602
  wifiRssi: {
@@ -5861,6 +5867,7 @@ var CAMERA_MEMBERS = {
5861
5867
  description: "Camera status LED. Video doorbells report the same state under their button-ring LED parameter.",
5862
5868
  write: (v, ctx) => statusLedCommand(asBool(v), ctx)
5863
5869
  },
5870
+ views: provided("media", (m) => m.views && (() => m.views), "Optical view identifiers supported by this camera's media route."),
5864
5871
  /**
5865
5872
  * Media is not a property: it returns DATA rather than moving state, its options are richer than a
5866
5873
  * value, and it exists only on a device bound to a provider. Each is declared once, with its
@@ -5881,10 +5888,12 @@ var CAMERA_MEMBERS = {
5881
5888
  snapshotStored: provided("media", (m, { ctx }) => isCameraCodec(ctx) && m.snapshotStored && (() => m.snapshotStored()), "Latest validated push thumbnail retained in memory.", ["snapshot"]),
5882
5889
  snapshotLive: provided("media", (m, { ctx, read }) => isCameraCodec(ctx) && (async (opts) => {
5883
5890
  refuseWhenDisabled(ctx, read);
5891
+ refuseUnsupportedView(m, opts);
5884
5892
  return m.snapshotLive({ powered: poweredOf(ctx), ...opts });
5885
5893
  }), "Fresh still decoded from a short live burst."),
5886
5894
  live: provided("media", (m, { ctx, read }) => async (opts) => {
5887
5895
  refuseWhenDisabled(ctx, read);
5896
+ refuseUnsupportedView(m, opts);
5888
5897
  return m.live({ powered: poweredOf(ctx), ...opts });
5889
5898
  }, "Open a managed live stream."),
5890
5899
  record: provided("media", (m, { ctx, read }) => isCameraCodec(ctx) && (async (...args) => {
@@ -5893,6 +5902,7 @@ var CAMERA_MEMBERS = {
5893
5902
  }), "Record N seconds \u2192 an mp4/h264 buffer."),
5894
5903
  openReadable: provided("media", (m, { ctx, read }) => isCameraCodec(ctx) && m.openReadable && (async (opts) => {
5895
5904
  refuseWhenDisabled(ctx, read);
5905
+ refuseUnsupportedView(m, opts);
5896
5906
  return m.openReadable({ powered: poweredOf(ctx), ...opts });
5897
5907
  }), "Open a node:stream Readable of the live feed."),
5898
5908
  recordFragments: provided("media", (m, { ctx, read }) => isCameraCodec(ctx) && m.recordFragments && ((opts) => {
@@ -6977,6 +6987,28 @@ var PTZ = {
6977
6987
 
6978
6988
  // dist/model/capabilities/doorbell.js
6979
6989
  import { setTimeout as sleep } from "node:timers/promises";
6990
+ var DOORBELL_MODELS = /* @__PURE__ */ new Set([
6991
+ "T820",
6992
+ "T8200",
6993
+ "T8201",
6994
+ "T8202",
6995
+ "T8203",
6996
+ "T8210",
6997
+ "T8212",
6998
+ "T8213",
6999
+ "T8214",
7000
+ "T8220",
7001
+ "T8221",
7002
+ "T8222"
7003
+ ]);
7004
+ var DOORBELL_TYPES = [
7005
+ DeviceType.DOORBELL,
7006
+ DeviceType.BATTERY_DOORBELL,
7007
+ DeviceType.BATTERY_DOORBELL_2,
7008
+ DeviceType.BATTERY_DOORBELL_PLUS,
7009
+ DeviceType.DOORBELL_SOLO,
7010
+ DeviceType.BATTERY_DOORBELL_PLUS_E340
7011
+ ];
6980
7012
  var DOORBELL_CMD = {
6981
7013
  /**
6982
7014
  * Doorbell "quick response" — make the doorbell PLAY a stored voice reply. Same 136-byte
@@ -7313,8 +7345,16 @@ var DOORBELL = {
7313
7345
  description: "Doorbell chime / ringtone configuration (ring press is an event, not a param).",
7314
7346
  members: DOORBELL_MEMBERS,
7315
7347
  properties: propertiesOf(DOORBELL_MEMBERS),
7316
- /** Doorbells self-report no single unambiguous param; the model name is the reliable signal. */
7317
- detection: { modelHints: [/doorbell/i] },
7348
+ /**
7349
+ * Doorbells self-report no param of their own (the 1702-1719 settings also arrive from eufyCams), and
7350
+ * the cloud record names no product, so detection keys off the app's own tables:
7351
+ * {@link DOORBELL_MODELS} and {@link DOORBELL_TYPES}.
7352
+ */
7353
+ detection: {
7354
+ detect: (rec) => DOORBELL_MODELS.has(rec.model ?? ""),
7355
+ deviceTypes: DOORBELL_TYPES,
7356
+ modelHints: [/doorbell/i]
7357
+ },
7318
7358
  /**
7319
7359
  * Inbound FCM doorbell events (`DoorbellPushEvent`): ring press and the package trio.
7320
7360
  *
@@ -11584,18 +11624,16 @@ var MODEL_REGISTRY = {
11584
11624
  T8002: { codec: "station", name: "HomeBase E" },
11585
11625
  // Cameras with curated extras that aren't always provable from a fresh device's params.
11586
11626
  T8423: { codec: "camera", caps: ["light", "battery"], name: "Floodlight Cam" },
11587
- T8210: { codec: "camera", caps: ["doorbell", "battery"], name: "Video Doorbell" },
11627
+ T8210: { codec: "camera", caps: ["battery"], name: "Video Doorbell" },
11588
11628
  // T8210 hardware variant, reported as its own code; shares the T8210 row.
11589
- T8210C: { codec: "camera", caps: ["doorbell", "battery"], name: "Video Doorbell" },
11629
+ T8210C: { codec: "camera", caps: ["battery"], name: "Video Doorbell" },
11590
11630
  // Confirmed against a real owned unit (named "Doorbell"): Video Doorbell Dual.
11591
- T8214: { codec: "camera", caps: ["doorbell", "battery"], name: "Video Doorbell Dual" },
11592
- // Battery Video Doorbell Dual, vendor deviceType 91 (BATTERY_DOORBELL_PLUS), reported by two users.
11593
- T8213: { codec: "camera", caps: ["doorbell", "battery"], name: "Video Doorbell Dual" },
11631
+ T8214: { codec: "camera", caps: ["battery"], name: "Video Doorbell Dual" },
11632
+ T8213: { codec: "camera", caps: ["battery"], name: "Video Doorbell Dual" },
11594
11633
  // Confirmed against a real owned unit: the mains-powered Wired Doorbell 2K. No `battery` row
11595
11634
  // member on purpose — this model is wired, so the codec baseline plus inference is the whole
11596
- // truth for power. Without this row it classified as a plain camera, so the doorbell capability
11597
- // never appeared and consumers got no doorbell event entity or ring trigger.
11598
- T8200: { codec: "camera", caps: ["doorbell"], name: "Wired Doorbell 2K" },
11635
+ // truth for power.
11636
+ T8200: { codec: "camera", name: "Wired Doorbell 2K" },
11599
11637
  // Cameras observed on real owned hardware (inspect-device sweep). Names from the app's own
11600
11638
  // model-family constants (scripts/data/app_model_registry.json); caps mirror what the device
11601
11639
  // reports. T8171 has no family entry in that dump, so it keeps the raw T-code.
@@ -23501,7 +23539,7 @@ var RtcLiveStream = class extends EventEmitter7 {
23501
23539
  audio_chn: nvr ? this.opts.channel : -1,
23502
23540
  streamtype: 2,
23503
23541
  stitch_mode: 1,
23504
- chn_list: [{ index: 0, chn: this.opts.channel, sensor: 0, isUps: 0, isClicked: true }],
23542
+ chn_list: [{ index: 0, chn: this.opts.channel, sensor: this.opts.view ?? 0, isUps: 0, isClicked: true }],
23505
23543
  pip_cord: { x1: 0, y1: 0, x2: 0, y2: 0 },
23506
23544
  station_video_type: 6,
23507
23545
  play_id: 1
@@ -24843,22 +24881,28 @@ var RtcCommandRouter = class {
24843
24881
  return this.callMediaProviderFor(route, route.stationSn, (session) => new RtcCallLiveStream(session, this.deps.logger), `record is not available for ${route.stationSn} over a portal call`, 45e3);
24844
24882
  }
24845
24883
  /** NVR media uses a dedicated call per camera, independent of station commands. */
24846
- nvrMediaProviderFor(route) {
24847
- return this.callMediaProviderFor(route, route.cameraSn, (session, accountId) => new RtcLiveStream({
24848
- session,
24849
- seg: new SegmentCounter(),
24850
- stationSn: route.stationSn,
24851
- channel: route.channel,
24852
- accountId,
24853
- logger: this.deps.logger,
24854
- nvr: true
24855
- }), "record is not available over an NVR portal call");
24884
+ nvrMediaProviderFor(route, views) {
24885
+ return {
24886
+ ...this.callMediaProviderFor(route, route.cameraSn, (session, accountId, view) => new RtcLiveStream({
24887
+ session,
24888
+ seg: new SegmentCounter(),
24889
+ stationSn: route.stationSn,
24890
+ channel: route.channel,
24891
+ accountId,
24892
+ logger: this.deps.logger,
24893
+ nvr: true,
24894
+ view
24895
+ }), "record is not available over an NVR portal call"),
24896
+ views
24897
+ };
24856
24898
  }
24857
24899
  /** Share a dedicated-call source and its consumer operations under one route key. */
24858
24900
  callMediaProviderFor(route, key, makeStream, recordError, warmTimeoutMs) {
24859
24901
  const sourceFor = (opts = {}) => {
24860
24902
  opts.signal?.throwIfAborted();
24861
- let source = this.callSources.get(key);
24903
+ const view = opts.view ?? 0;
24904
+ const sourceKey = view === 0 ? key : `${key}:view:${view}`;
24905
+ let source = this.callSources.get(sourceKey);
24862
24906
  if (!source) {
24863
24907
  source = new SharedLiveSource({
24864
24908
  makeStream: () => {
@@ -24866,15 +24910,15 @@ var RtcCommandRouter = class {
24866
24910
  if (!identity)
24867
24911
  throw new Error(`rtc: not logged in, cannot call ${route.stationSn}`);
24868
24912
  const accountId = route.adminUserId || identity.userId;
24869
- return makeStream(this.newSession(route, accountId, identity), accountId);
24913
+ return makeStream(this.newSession(route, accountId, identity), accountId, view);
24870
24914
  },
24871
- label: key,
24915
+ label: sourceKey,
24872
24916
  logger: this.deps.logger,
24873
24917
  powered: opts.powered,
24874
24918
  preBufferSeconds: opts.preBufferSeconds,
24875
24919
  warmTimeoutMs
24876
24920
  });
24877
- this.callSources.set(key, source);
24921
+ this.callSources.set(sourceKey, source);
24878
24922
  }
24879
24923
  return source;
24880
24924
  };
@@ -29447,7 +29491,7 @@ var EufyMega = class extends EventEmitter14 {
29447
29491
  try {
29448
29492
  return await media.snapshotLive(opts);
29449
29493
  } catch (error) {
29450
- if (!(error instanceof LiveSnapshotUnavailableError))
29494
+ if (!(error instanceof LiveSnapshotUnavailableError) || (opts?.view ?? 0) !== 0)
29451
29495
  throw error;
29452
29496
  const retained = await retainedStill().catch(() => void 0);
29453
29497
  const geometry = retained && jpegGeometry(retained);
@@ -29536,7 +29580,7 @@ var EufyMega = class extends EventEmitter14 {
29536
29580
  if (route.signalingMode === "call") {
29537
29581
  if (!camera || !isNvrMediaCamera(camera))
29538
29582
  return void 0;
29539
- return this.rtc.nvrMediaProviderFor({ ...route, cameraSn: sn, channel });
29583
+ return this.rtc.nvrMediaProviderFor({ ...route, cameraSn: sn, channel }, isNvrDualLensCamera(camera) ? [0, 1] : [0]);
29540
29584
  }
29541
29585
  return this.rtc.mediaProviderFor({ ...route, cameraSn: sn, channel });
29542
29586
  }