@mega-yfue/eufy-sdk 0.5.0-beta.21 → 0.5.0-beta.22

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.
@@ -418,9 +418,10 @@ export declare class EufyMega extends EventEmitter {
418
418
  * The `eufy_life` DP writes (smart lights) are secure-MQTT-only. `aiot-dp` routes to either the
419
419
  * Anker AIoT MQTT stack or the legacy Tuya REST router depending on the device's category
420
420
  * (`eufy_home_tuya` → Tuya, everything else → MQTT). The capability layer emits a single `aiot-dp`
421
- * kind and stays transport-agnostic; only the facade sees both sides and decides here. A T9000
421
+ * kind and stays transport-agnostic; only the facade sees both sides and decides here. A T9000 or T8N00
422
422
  * station and the devices attached to it go over RTC, with an attached device's command refused
423
- * unless its channel resolves back to it on the station. Everything else is P2P.
423
+ * unless its channel resolves back to it on the station. T8N00 uses full SDP and admits direct and
424
+ * relay candidates; T9000 retains compact SDP and relay-only candidates. Everything else is P2P.
424
425
  */
425
426
  private routeCommand;
426
427
  /**
@@ -55,6 +55,8 @@ export interface EufyDevice {
55
55
  * every attached sensor of a T8010 — and serves only as a fallback for a device naming no parent.
56
56
  */
57
57
  stationSn?: string;
58
+ /** The device's channel on its station, as its record states it (`device_channel`); absent where it states none. */
59
+ channel?: number;
58
60
  /** Present (and non-empty) for P2P devices. */
59
61
  p2pDid?: string;
60
62
  /**
package/dist/index.js CHANGED
@@ -3068,8 +3068,7 @@ var STATION_TYPES = /* @__PURE__ */ new Set([
3068
3068
  DeviceType.STATION_9000,
3069
3069
  DeviceType.MINIBASE_CHIME,
3070
3070
  DeviceType.HOMEBASE_MINI,
3071
- DeviceType.NVR_S4_MAX,
3072
- DeviceType.CAMERA_POE_S4
3071
+ DeviceType.NVR_S4_MAX
3073
3072
  ]);
3074
3073
  var LOCK_TYPES = /* @__PURE__ */ new Set([
3075
3074
  DeviceType.LOCK_BLE,
@@ -3143,7 +3142,7 @@ function codecFromModel(model) {
3143
3142
  return "vacuum";
3144
3143
  if (/^T8(00[0-9]|010|023|025|030)/.test(m))
3145
3144
  return "station";
3146
- if (/^T8N0/.test(m) || /^T8E0/.test(m))
3145
+ if (/^T8N0/.test(m))
3147
3146
  return "station";
3148
3147
  if (/^T9000/.test(m))
3149
3148
  return "station";
@@ -5095,6 +5094,7 @@ var isIndoorCamMini = (ctx) => ctx.deviceType === DeviceType.INDOOR_COST_DOWN_CA
5095
5094
  var isIndoorPanTiltS350 = (ctx) => has(INDOOR_PT_S350_TYPES, ctx.deviceType);
5096
5095
  var isHomeBase = (ctx) => has(HOMEBASE_TYPES, ctx.deviceType);
5097
5096
  var isStation9000 = (ctx) => ctx.deviceType === DeviceType.STATION_9000;
5097
+ var isNvrT8N00 = (ctx) => ctx.model === "T8N00";
5098
5098
  var isAiotVacuum = (ctx) => ctx.category !== "eufy_home_tuya";
5099
5099
  var isTuyaVacuum = (ctx) => ctx.category === "eufy_home_tuya";
5100
5100
 
@@ -7683,7 +7683,8 @@ var ARMING_CMD = {
7683
7683
  */
7684
7684
  SET_ARMING: 1224,
7685
7685
  /**
7686
- * The per-mode alarm/arm-delay configuration write. ✅ WIRE CAPTURED live on a T8030 (
7686
+ * One guard mode's per-device action table (app `CMD_SET_ALL_ACTION`), which carries the alarm delays
7687
+ * and each device's actions in that mode. ✅ WIRE CAPTURED live on a T8030 (
7687
7688
  * 2026-07-23, both directions): a **bare
7688
7689
  * JSON frame, no `1350`/`1700` envelope** — outer P2P cmd IS `1255` itself, station channel 255,
7689
7690
  * plaintext `{account_id, count_down_alarm:{channel_list,delay_time},
@@ -7700,15 +7701,26 @@ var ARMING_CMD = {
7700
7701
  * corresponding GET command (`1310`/inner cmd `40003`, sent by the app right before editing) always
7701
7702
  * replied `{count:0,data:null}` in every capture — genuinely empty, not a decrypt failure (confirmed
7702
7703
  * via the same bidirectional decrypt this finding used) — so the app does NOT read the current
7703
- * config this way; how it does is still unknown. Without a working GET, safely PATCHING just one
7704
- * channel in or out of an existing list isn't possible without risking clobbering the rest — so
7704
+ * config this way; a T8010 reports it on its own params instead, see below. Without a working GET,
7705
+ * safely PATCHING just one channel in or out of an existing list isn't possible without risking
7706
+ * clobbering the rest — so
7705
7707
  * this ships as a caller-supplies-everything write instead of guessing a merge.
7706
7708
  * `devices`/`siren_sensor_action` are even less understood (raw per-device action codes, meaning
7707
7709
  * unconfirmed) and MUST come from a value independently read/captured for the target mode —
7708
7710
  * see `AlarmDelayConfig`'s field docs.
7711
+ *
7712
+ * ✅ On a T8010 the app writes the same command for the Device Control screen: header channel 0, the
7713
+ * whole table of the mode `{mode_id, devices, count_down_alarm, count_down_arm}`, acked by an all-zero
7714
+ * 1255 reply. The T8010 reports each table on its own params, Home on 1157 and Away on 1158, and the
7715
+ * reported table follows a write within 15 s. See {@link DEVICE_NOTIFY_FLAG}.
7709
7716
  */
7710
- ALARM_DELAY_CONFIG: 1255
7717
+ SET_ALL_ACTION: 1255
7711
7718
  };
7719
+ var DEVICE_NOTIFY_FLAG = 8;
7720
+ function actionTable(read, mode) {
7721
+ const value = read(`${mode}ActionTable`)?.value;
7722
+ return value && typeof value === "object" && Array.isArray(value.devices) ? value : void 0;
7723
+ }
7712
7724
  var ARMING_MODE_WIRE = {
7713
7725
  away: 0,
7714
7726
  home: 1,
@@ -7745,7 +7757,15 @@ function alarmDelayCommand(mode, config, ctx) {
7745
7757
  devices: config.devices.map((d) => ({ action: d.action, device_channel: d.deviceChannel })),
7746
7758
  siren_sensor_action: config.sirenSensorAction.map((d) => ({ action: d.action, device_channel: d.deviceChannel }))
7747
7759
  };
7748
- return setJsonRaw(ARMING_CMD.ALARM_DELAY_CONFIG, data, ctx, STATION_CHANNEL2);
7760
+ return setJsonRaw(ARMING_CMD.SET_ALL_ACTION, data, ctx, STATION_CHANNEL2);
7761
+ }
7762
+ var ACTION_TABLE_LAG_MS = 15e3;
7763
+ function withDeviceNotification(table, channel, on) {
7764
+ if (!table?.devices.some((d) => d.device_channel === channel))
7765
+ return void 0;
7766
+ const { account_id: _account, ...rest } = table;
7767
+ const devices = table.devices.map((d) => d.device_channel === channel ? { ...d, action: on ? d.action | DEVICE_NOTIFY_FLAG : d.action & ~DEVICE_NOTIFY_FLAG } : d);
7768
+ return { ...rest, devices };
7749
7769
  }
7750
7770
  var ARMING_MEMBERS = {
7751
7771
  /**
@@ -7803,7 +7823,45 @@ var ARMING_MEMBERS = {
7803
7823
  } catch (e) {
7804
7824
  return Promise.reject(e);
7805
7825
  }
7806
- }, "Write the full per-mode alarm/arm-delay configuration.")
7826
+ }, "Write the full per-mode alarm/arm-delay configuration."),
7827
+ /**
7828
+ * Whether the device on `channel` sends a push notification in `mode`: its {@link DEVICE_NOTIFY_FLAG}
7829
+ * in the action table the station reports for that mode. `undefined` where the station reports no table
7830
+ * or the table does not list `channel`. A device's channel is `EufyDevice.channel`.
7831
+ */
7832
+ deviceNotification: method(({ read }) => (mode, channel) => {
7833
+ const action = actionTable(read, mode)?.devices.find((d) => d.device_channel === channel)?.action;
7834
+ return typeof action === "number" ? (action & DEVICE_NOTIFY_FLAG) !== 0 : void 0;
7835
+ }, "Whether the device on a channel sends a push notification in a guard mode (home/away).", (ctx) => ctx.model === "T8010"),
7836
+ /**
7837
+ * Turn the push notification of the device on `channel` on or off in `mode` (cmd
7838
+ * {@link ARMING_CMD.SET_ALL_ACTION}): the station's reported table written back with that one flag
7839
+ * changed. Rejects, sending nothing, where the table does not list `channel`. A write builds on the last
7840
+ * one sent for that mode until the station reports a table observed 15 s after it, so a second write does
7841
+ * not revert it; a write whose dispatch rejects is not built on.
7842
+ */
7843
+ setDeviceNotification: method(({ ctx, sink, read }) => {
7844
+ const sent = /* @__PURE__ */ new Map();
7845
+ return (mode, channel, on) => {
7846
+ const last = sent.get(mode);
7847
+ const observed = read(`${mode}ActionTable`)?.ts ?? 0;
7848
+ const base = last && observed < last.at + ACTION_TABLE_LAG_MS ? last.table : actionTable(read, mode);
7849
+ const table = withDeviceNotification(base, channel, on);
7850
+ if (!table) {
7851
+ return Promise.reject(new Error(`arming: no ${mode} action table lists channel ${channel} [${describeDevice(ctx)}]`));
7852
+ }
7853
+ sent.set(mode, { table, at: Date.now() });
7854
+ return sink.dispatch(setJsonRaw(ARMING_CMD.SET_ALL_ACTION, table, ctx, 0)).catch((err) => {
7855
+ if (sent.get(mode)?.table === table) {
7856
+ if (last)
7857
+ sent.set(mode, last);
7858
+ else
7859
+ sent.delete(mode);
7860
+ }
7861
+ throw err;
7862
+ });
7863
+ };
7864
+ }, "Turn the push notification of the device on a channel on or off in a guard mode (home/away).", (ctx) => ctx.model === "T8010")
7807
7865
  };
7808
7866
  var ARMING = {
7809
7867
  capability: "arming",
@@ -11439,9 +11497,15 @@ var SECURITY_PARAMS = {
11439
11497
  provenance: "guessed"
11440
11498
  },
11441
11499
  1157: {
11442
- name: "jsDelayHome",
11500
+ name: "homeActionTable",
11443
11501
  type: "string",
11444
- provenance: "apk",
11502
+ provenance: "verified",
11503
+ encoding: "base64+json"
11504
+ },
11505
+ 1158: {
11506
+ name: "awayActionTable",
11507
+ type: "string",
11508
+ provenance: "verified",
11445
11509
  encoding: "base64+json"
11446
11510
  },
11447
11511
  1159: {
@@ -15367,7 +15431,7 @@ var PADDED_RESULT_BYTES = 132;
15367
15431
  var CMD_CAMERA_INFO = 1103;
15368
15432
  var CMD_DATABASE_IMAGE = 1308;
15369
15433
  var CMD_DATABASE = 1306;
15370
- var DB_QUERY = { FULL_TABLE: 1e4, COMBINATION_WITH_AI: 10011 };
15434
+ var DB_QUERY = { FULL_TABLE: 1e4, COMBINATION_WITH_AI: 10011, QUERY_LOCAL: 10017 };
15371
15435
  var GCM_AAD = Buffer.from("eufy security");
15372
15436
  var LEVEL2_SEQ_BASE = 16909056;
15373
15437
  function paramReport(payload) {
@@ -15397,9 +15461,13 @@ function firstTableRows(text2) {
15397
15461
  } catch {
15398
15462
  continue;
15399
15463
  }
15400
- const data = parsed?.data;
15401
- if (Array.isArray(data))
15402
- return data;
15464
+ const reply = parsed;
15465
+ if (typeof reply?.mIntRet === "number" && reply.mIntRet !== 0)
15466
+ return { error: reply.mIntRet };
15467
+ if (reply?.data === "[]")
15468
+ return { rows: [] };
15469
+ if (Array.isArray(reply?.data))
15470
+ return { rows: reply.data };
15403
15471
  }
15404
15472
  return void 0;
15405
15473
  }
@@ -16021,6 +16089,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
16021
16089
  socket.removeAllListeners("error");
16022
16090
  socket.on("error", (error) => this.emit("error", error));
16023
16091
  this.socket = socket;
16092
+ this.requestReceiveBuffer(socket);
16024
16093
  }
16025
16094
  this.connected = true;
16026
16095
  this.connectedAtMs = Date.now();
@@ -16759,6 +16828,29 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
16759
16828
  query: this.fullTableQuery()
16760
16829
  });
16761
16830
  }
16831
+ /**
16832
+ * Read a date-ranged page of `history_record_info` from the station. The reply's `data` array groups
16833
+ * recording rows with companion tables; each group retains its `table_name` and `payload`.
16834
+ * `startTime` is `"0"` for the newest page or the oldest row's `yyyyMMddHHmmss` cursor for older rows.
16835
+ */
16836
+ queryRecordPage(opts) {
16837
+ return this.readDatabase("history_record_info", {
16838
+ accountId: opts.accountId,
16839
+ innerCmd: DB_QUERY.QUERY_LOCAL,
16840
+ query: {
16841
+ count: opts.count ?? 20,
16842
+ start_date: opts.startDate,
16843
+ end_date: opts.endDate,
16844
+ start_time: opts.startTime ?? "0",
16845
+ event_type: 0,
16846
+ ai_type: 0,
16847
+ storage_cloud: -1,
16848
+ trigger_type: 0,
16849
+ detection_type: 0,
16850
+ flag: 0
16851
+ }
16852
+ });
16853
+ }
16762
16854
  /**
16763
16855
  * Query one on-station table and answer its rows, once the reply is whole.
16764
16856
  *
@@ -16766,7 +16858,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
16766
16858
  * several frames whose decrypted text is a fragment of one document, so the fragments are
16767
16859
  * accumulated here and scanned after each one. Answers that document's `data` rows. Rejects when
16768
16860
  * `signal` aborts, and when `timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}) elapses with no
16769
- * complete reply.
16861
+ * complete reply, and when the station answers a non-zero `mIntRet`.
16770
16862
  *
16771
16863
  * One read at a time per session: while one is accumulating, every {@link queryDatabase} on the
16772
16864
  * session throws, so a second reply cannot land in this buffer.
@@ -16774,16 +16866,22 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
16774
16866
  async readDatabase(table, opts = {}) {
16775
16867
  if (opts.signal?.aborted)
16776
16868
  throw new Error("readDatabase: aborted");
16777
- this.queryDatabase(table, { accountId: opts.accountId, query: this.fullTableQuery() });
16869
+ this.queryDatabase(table, {
16870
+ accountId: opts.accountId,
16871
+ innerCmd: opts.innerCmd,
16872
+ query: opts.query ?? this.fullTableQuery()
16873
+ });
16778
16874
  this.dbReadInFlight = true;
16779
16875
  try {
16780
16876
  return await new Promise((resolve, reject) => {
16781
16877
  let text2 = "";
16782
16878
  const onChunk = (chunk) => {
16783
16879
  text2 += chunk.text;
16784
- const rows = firstTableRows(text2);
16785
- if (rows)
16786
- settle(() => resolve(rows));
16880
+ const result = firstTableRows(text2);
16881
+ if (result?.error !== void 0)
16882
+ settle(() => reject(new Error(`readDatabase: ${table} refused: ${result.error}`)));
16883
+ else if (result?.rows)
16884
+ settle(() => resolve(result.rows));
16787
16885
  };
16788
16886
  const timer = setTimeout(() => settle(() => reject(new Error(`readDatabase: ${this.cfg.stationSn} sent no complete ${table}`))), opts.timeoutMs ?? DB_TABLE_TIMEOUT_MS);
16789
16887
  const onAbort = () => settle(() => reject(new Error("readDatabase: aborted")));
@@ -20844,16 +20942,12 @@ function stationOf(dev) {
20844
20942
  const parent = typeof raw.parent_sn === "string" && raw.parent_sn && raw.parent_sn !== dev.sn ? raw.parent_sn : void 0;
20845
20943
  return parent ?? dev.stationSn ?? resolvedStationSn(raw, dev.sn);
20846
20944
  }
20847
- function statedChannel(dev) {
20848
- const v = dev.raw?.device_channel;
20849
- return typeof v === "number" ? v : void 0;
20850
- }
20851
20945
  function stationChannels(devices) {
20852
20946
  const out = /* @__PURE__ */ new Map();
20853
20947
  const claimants = /* @__PURE__ */ new Map();
20854
20948
  for (const d of devices) {
20855
20949
  const station = stationOf(d);
20856
- const stated = statedChannel(d);
20950
+ const stated = d.channel;
20857
20951
  if (d.sn === station || stated === void 0)
20858
20952
  continue;
20859
20953
  const counts = claimants.get(station) ?? /* @__PURE__ */ new Map();
@@ -20862,7 +20956,7 @@ function stationChannels(devices) {
20862
20956
  }
20863
20957
  for (const d of devices) {
20864
20958
  const station = stationOf(d);
20865
- const stated = statedChannel(d);
20959
+ const stated = d.channel;
20866
20960
  if (d.sn === station)
20867
20961
  out.set(d.sn, { channel: stated ?? 0 });
20868
20962
  else if (stated === void 0)
@@ -22264,7 +22358,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
22264
22358
  * **`set-json-raw` intent** — bare JSON, no envelope: outer P2P cmd = `outerCmd` itself, plaintext
22265
22359
  * exactly `{account_id,...data}` (`session.sendRawLevel2` with no wrapper). Reversed from a live
22266
22360
  * capture of the app's own SET_SNOOZE_TIME (1271) frame — see `param-dictionary.ts`'s `1271` entry
22267
- * (`snoozeTime`); the alarm-delay config (1255, `arming.ts`'s `ARMING_CMD.ALARM_DELAY_CONFIG`) reuses
22361
+ * (`snoozeTime`); the alarm-delay config (1255, `arming.ts`'s `ARMING_CMD.SET_ALL_ACTION`) reuses
22268
22362
  * the same bare-JSON shape.
22269
22363
  */
22270
22364
  async sendJsonRaw(sn, outerCmd, data, channel) {
@@ -22927,6 +23021,7 @@ var RtcPeer = class extends EventEmitter7 {
22927
23021
  wireTally = /* @__PURE__ */ new Map();
22928
23022
  commandOpen = false;
22929
23023
  remoteSet = false;
23024
+ remoteMid = HUB_SDP_MID;
22930
23025
  handlingOffer = false;
22931
23026
  channelsCreated = false;
22932
23027
  gatheringDone = false;
@@ -22947,7 +23042,7 @@ var RtcPeer = class extends EventEmitter7 {
22947
23042
  const createPeer = this.opts.createPeer ?? await loadNativePeerFactory();
22948
23043
  const config = {
22949
23044
  iceServers: turnServers(turn),
22950
- iceTransportPolicy: "relay",
23045
+ iceTransportPolicy: this.opts.iceTransportPolicy ?? "relay",
22951
23046
  maxMessageSize: ANKER_MAX_MESSAGE_SIZE,
22952
23047
  enableIceTcp: true
22953
23048
  };
@@ -22998,6 +23093,8 @@ var RtcPeer = class extends EventEmitter7 {
22998
23093
  this.handlingOffer = true;
22999
23094
  try {
23000
23095
  const offer = forceDtlsRole(offerSdp, "passive");
23096
+ const application = offer.split(/(?=^m=)/m).find((section) => section.startsWith("m=application "));
23097
+ this.remoteMid = application?.match(/^a=mid:([^\r\n]+)/m)?.[1] ?? HUB_SDP_MID;
23001
23098
  const answerWait = new Promise((resolve, reject) => {
23002
23099
  const timer = setTimeout(() => {
23003
23100
  this.localAnswer = void 0;
@@ -23070,7 +23167,7 @@ var RtcPeer = class extends EventEmitter7 {
23070
23167
  this.pending.length = 0;
23071
23168
  }
23072
23169
  acceptsCandidate(candidate) {
23073
- return iceCandidateType(candidate) === "relay";
23170
+ return this.opts.iceTransportPolicy === "all" || iceCandidateType(candidate) === "relay";
23074
23171
  }
23075
23172
  createChannels() {
23076
23173
  if (!this.pc || this.channelsCreated)
@@ -23126,7 +23223,7 @@ var RtcPeer = class extends EventEmitter7 {
23126
23223
  }
23127
23224
  addNow(candidate) {
23128
23225
  try {
23129
- this.pc?.addRemoteCandidate(candidate, HUB_SDP_MID);
23226
+ this.pc?.addRemoteCandidate(candidate, this.remoteMid);
23130
23227
  } catch (e) {
23131
23228
  this.logger.warn(`[rtc] addRemoteCandidate failed: ${e instanceof Error ? e.message : String(e)}`);
23132
23229
  }
@@ -23303,14 +23400,14 @@ var RtcSignalingClient = class extends EventEmitter8 {
23303
23400
  this.sendEnvelope(`${this.opts.authToken}_${this.makeMsgId()}`, inner);
23304
23401
  }
23305
23402
  sendCall() {
23306
- this.sendSession("scall");
23403
+ this.sendSession(this.opts.signalingMode ?? "scall");
23307
23404
  }
23308
23405
  sendAck() {
23309
23406
  this.sendSession("ack");
23310
23407
  }
23311
- /** The SDP answer, as scall JSON text, in an `info`. */
23312
- sendInfoSdp(scallJson) {
23313
- this.sendSession("info", { sdp: scallJson });
23408
+ /** The SDP answer, encoded for the negotiated call format, in an `info`. */
23409
+ sendInfoSdp(sdp) {
23410
+ this.sendSession("info", { sdp });
23314
23411
  }
23315
23412
  /** Trickle a candidate on the session's channel, like its SDP answer. An empty candidate ends them. */
23316
23413
  sendInfoCandidate(candidate) {
@@ -23438,7 +23535,7 @@ var RtcSession = class extends EventEmitter9 {
23438
23535
  return this.connected;
23439
23536
  }
23440
23537
  /**
23441
- * Start the sequence; resolves once `scall` is sent. `connected` fires when the channel opens. A session
23538
+ * Start the sequence; resolves once the call is sent. `connected` fires when the channel opens. A session
23442
23539
  * closed by the time the sign fetch or the auth completes rejects instead of opening the socket or
23443
23540
  * placing the call.
23444
23541
  */
@@ -23450,7 +23547,7 @@ var RtcSession = class extends EventEmitter9 {
23450
23547
  await this.waitForAuth();
23451
23548
  if (this.closed)
23452
23549
  throw new Error("RTC session closed before the call was placed");
23453
- this.logger.debug(`[rtc] ${this.opts.stationSn} authenticated \u2014 scall`);
23550
+ this.logger.debug(`[rtc] ${this.opts.stationSn} authenticated \u2014 ${this.opts.signalingMode ?? "scall"}`);
23454
23551
  this.signaling.sendCall();
23455
23552
  }
23456
23553
  /** Send one portal packet; false when the command channel isn't open. */
@@ -23587,7 +23684,7 @@ var RtcSession = class extends EventEmitter9 {
23587
23684
  offer = sdpText;
23588
23685
  }
23589
23686
  const answer = await this.peer.handleRemoteOffer(offer);
23590
- this.signaling.sendInfoSdp(JSON.stringify(sdpToScallJson(answer)));
23687
+ this.signaling.sendInfoSdp(this.opts.signalingMode === "call" ? answer : JSON.stringify(sdpToScallJson(answer)));
23591
23688
  this.logger.debug(`[rtc] ${this.opts.stationSn} answered the hub's offer`);
23592
23689
  }
23593
23690
  };
@@ -23608,7 +23705,7 @@ var RtcCommandRouter = class {
23608
23705
  }
23609
23706
  async dispatchCommand(route, cmd) {
23610
23707
  if (cmd.kind !== "set-payload" && cmd.kind !== "set-json") {
23611
- throw new Error(`rtc: ${cmd.kind} is not routable over the T9000 control channel (only set-payload or set-json)`);
23708
+ throw new Error(`rtc: ${cmd.kind} is not routable over the RTC control channel (only set-payload or set-json)`);
23612
23709
  }
23613
23710
  const identity = this.deps.identity();
23614
23711
  if (!identity)
@@ -23616,7 +23713,7 @@ var RtcCommandRouter = class {
23616
23713
  const { stationSn } = route;
23617
23714
  const adminUserId = route.adminUserId || identity.userId;
23618
23715
  const channel = route.attached ? cmd.channel : PORTAL_STATION_CHANNEL;
23619
- const st = await this.stationSession(stationSn, adminUserId, identity);
23716
+ const st = await this.stationSession(route, adminUserId, identity);
23620
23717
  const outerCmd = cmd.kind === "set-json" ? PORTAL_CMD_CONTROL_PAYLOAD : PORTAL_CMD_SET_PAYLOAD;
23621
23718
  const innerCmd = cmd.kind === "set-json" ? cmd.param : cmd.cmd;
23622
23719
  const payload = cmd.kind === "set-json" ? { account_id: adminUserId, cmd: cmd.param, commandType: cmd.param, data: cmd.data } : { account_id: adminUserId, cmd: cmd.cmd, mValue3: cmd.mValue3 ?? 0, payload: cmd.payload };
@@ -23677,7 +23774,8 @@ var RtcCommandRouter = class {
23677
23774
  }
23678
23775
  });
23679
23776
  }
23680
- async stationSession(sn, adminUserId, identity) {
23777
+ async stationSession(route, adminUserId, identity) {
23778
+ const sn = route.stationSn;
23681
23779
  const existing = this.sessions.get(sn);
23682
23780
  if (existing) {
23683
23781
  await existing.ready;
@@ -23695,7 +23793,8 @@ var RtcCommandRouter = class {
23695
23793
  shard: this.deps.shard(),
23696
23794
  country: this.deps.country ?? "US",
23697
23795
  logger: this.deps.logger,
23698
- peer: { logger: this.deps.logger }
23796
+ signalingMode: route.signalingMode,
23797
+ peer: { logger: this.deps.logger, iceTransportPolicy: route.iceTransportPolicy }
23699
23798
  });
23700
23799
  const ready = this.bringUp(sn, session);
23701
23800
  const st = { session, seg: new SegmentCounter(), ready, queue: Promise.resolve() };
@@ -26556,6 +26655,7 @@ var DeviceRegistry = class {
26556
26655
  name: recordName(raw, deviceClass),
26557
26656
  model: raw.device_model,
26558
26657
  stationSn: resolvedStationSn(raw, raw.device_sn),
26658
+ ...typeof raw.device_channel === "number" ? { channel: raw.device_channel } : {},
26559
26659
  p2pDid: raw.p2p_did,
26560
26660
  params,
26561
26661
  paramUpdatedAt,
@@ -27908,9 +28008,10 @@ var EufyMega = class extends EventEmitter12 {
27908
28008
  * The `eufy_life` DP writes (smart lights) are secure-MQTT-only. `aiot-dp` routes to either the
27909
28009
  * Anker AIoT MQTT stack or the legacy Tuya REST router depending on the device's category
27910
28010
  * (`eufy_home_tuya` → Tuya, everything else → MQTT). The capability layer emits a single `aiot-dp`
27911
- * kind and stays transport-agnostic; only the facade sees both sides and decides here. A T9000
28011
+ * kind and stays transport-agnostic; only the facade sees both sides and decides here. A T9000 or T8N00
27912
28012
  * station and the devices attached to it go over RTC, with an attached device's command refused
27913
- * unless its channel resolves back to it on the station. Everything else is P2P.
28013
+ * unless its channel resolves back to it on the station. T8N00 uses full SDP and admits direct and
28014
+ * relay candidates; T9000 retains compact SDP and relay-only candidates. Everything else is P2P.
27914
28015
  */
27915
28016
  routeCommand(sn, cmd) {
27916
28017
  if (cmd.kind === "ff09-actuate" || cmd.kind === "ff09-autolock" || cmd.kind === "ff09-setting-toggle") {
@@ -27936,13 +28037,16 @@ var EufyMega = class extends EventEmitter12 {
27936
28037
  const station = devices.find((d) => d.sn === stationSn);
27937
28038
  const stationRaw = station?.raw ?? {};
27938
28039
  const deviceType = typeof stationRaw.device_type === "number" ? stationRaw.device_type : void 0;
27939
- if (target && station && isStation9000({ deviceType, model: station.model })) {
28040
+ const family = { deviceType, model: station?.model };
28041
+ const native = isNvrT8N00(family);
28042
+ if (target && station && (isStation9000(family) || native)) {
27940
28043
  const attached = stationSn !== sn;
27941
28044
  if (attached && this.registry.serialForFrame(stationSn, cmd.channel) !== sn)
27942
28045
  return Promise.reject(new Error("RTC command requires an unambiguous attached-device channel"));
27943
28046
  const member = stationRaw.member?.admin_user_id;
27944
28047
  const adminUserId = typeof member === "string" && member ? member : void 0;
27945
- return this.rtc.dispatchCommand({ stationSn, adminUserId, attached }, cmd);
28048
+ const base = { stationSn, adminUserId, attached };
28049
+ return this.rtc.dispatchCommand(native ? { ...base, signalingMode: "call", iceTransportPolicy: "all" } : base, cmd);
27946
28050
  }
27947
28051
  return this.p2p.dispatchCommand(sn, cmd);
27948
28052
  }