@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.
- package/dist/client/eufy-mega.d.ts +3 -2
- package/dist/core/types.d.ts +2 -0
- package/dist/index.js +149 -45
- package/dist/index.js.map +2 -2
- package/dist/model/capabilities/arming.d.ts +31 -4
- package/dist/model/classify.d.ts +1 -2
- package/dist/model/device-family.d.ts +2 -0
- package/dist/transport/p2p/command-router.d.ts +1 -1
- package/dist/transport/p2p/p2p-session.d.ts +15 -1
- package/dist/transport/rtc/command-router.d.ts +5 -2
- package/dist/transport/rtc/peer.d.ts +4 -1
- package/dist/transport/rtc/session.d.ts +1 -1
- package/dist/transport/rtc/signaling.d.ts +4 -2
- package/package.json +1 -1
|
@@ -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.
|
|
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
|
/**
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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)
|
|
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
|
-
*
|
|
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;
|
|
7704
|
-
* channel in or out of an existing list isn't possible without risking
|
|
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
|
-
|
|
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.
|
|
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: "
|
|
11500
|
+
name: "homeActionTable",
|
|
11443
11501
|
type: "string",
|
|
11444
|
-
provenance: "
|
|
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
|
|
15401
|
-
if (
|
|
15402
|
-
return
|
|
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, {
|
|
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
|
|
16785
|
-
if (
|
|
16786
|
-
settle(() =>
|
|
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 =
|
|
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 =
|
|
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.
|
|
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,
|
|
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,
|
|
23312
|
-
sendInfoSdp(
|
|
23313
|
-
this.sendSession("info", { sdp
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|