@mega-yfue/eufy-sdk 0.5.0-beta.21 → 0.5.0-beta.23
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 +166 -54
- package/dist/index.js.map +2 -2
- package/dist/model/capabilities/arming.d.ts +31 -4
- package/dist/model/capabilities/dp-catalog.d.ts +4 -3
- package/dist/model/capabilities/suction.d.ts +5 -5
- 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",
|
|
@@ -10447,6 +10505,11 @@ var SuctionLevel = {
|
|
|
10447
10505
|
};
|
|
10448
10506
|
var SUCTION_LABEL = enumLabels(SuctionLevel);
|
|
10449
10507
|
var ALL_SUCTION_LEVELS = [0, 1, 2, 3, 4, 5];
|
|
10508
|
+
function levelFromCatalog(member) {
|
|
10509
|
+
if (Object.hasOwn(SuctionLevel, member))
|
|
10510
|
+
return SuctionLevel[member];
|
|
10511
|
+
return Object.hasOwn(SUCTION_LABEL, member) ? Number(member) : void 0;
|
|
10512
|
+
}
|
|
10450
10513
|
function suctionLevelName(value) {
|
|
10451
10514
|
return SUCTION_LABEL[value];
|
|
10452
10515
|
}
|
|
@@ -10507,13 +10570,15 @@ var SUCTION = {
|
|
|
10507
10570
|
},
|
|
10508
10571
|
/**
|
|
10509
10572
|
* The per-model level range — sourced from the SKU's `get_product_data_point` catalog for DP 158
|
|
10510
|
-
* when available. `undefined` when no catalog entry exists for DP 158
|
|
10573
|
+
* when available. `undefined` when no catalog entry exists for DP 158, or when no listed member maps
|
|
10574
|
+
* to a known level (so a caller never reads `[]` as "nothing selectable"). `setSuctionLevel` is only
|
|
10511
10575
|
* installed on AIoT devices (gated by `available: isAiotVacuum` on the `level` entry), so
|
|
10512
10576
|
* `supportedLevels` is harmless data on a non-AIoT device.
|
|
10513
10577
|
*/
|
|
10514
10578
|
actions({ ctx }) {
|
|
10515
10579
|
const catalogRange = ctx.dpCatalog?.enumRanges.get(SUCTION_DP.SUCTION);
|
|
10516
|
-
const
|
|
10580
|
+
const levels = catalogRange?.flatMap((m) => levelFromCatalog(m) ?? []);
|
|
10581
|
+
const supportedLevels = levels?.length ? levels : void 0;
|
|
10517
10582
|
return { supportedLevels };
|
|
10518
10583
|
}
|
|
10519
10584
|
};
|
|
@@ -10555,28 +10620,29 @@ function parseEnumRange(values) {
|
|
|
10555
10620
|
if (values === null || values === void 0)
|
|
10556
10621
|
return [];
|
|
10557
10622
|
if (Array.isArray(values))
|
|
10558
|
-
return
|
|
10623
|
+
return toMemberArray(values);
|
|
10559
10624
|
if (typeof values === "string") {
|
|
10560
10625
|
try {
|
|
10561
10626
|
const parsed = JSON.parse(values);
|
|
10562
10627
|
if (Array.isArray(parsed))
|
|
10563
|
-
return
|
|
10628
|
+
return toMemberArray(parsed);
|
|
10564
10629
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
10565
10630
|
const obj = parsed;
|
|
10566
10631
|
if (Array.isArray(obj.range))
|
|
10567
|
-
return
|
|
10632
|
+
return toMemberArray(obj.range);
|
|
10568
10633
|
}
|
|
10569
10634
|
} catch {
|
|
10570
10635
|
}
|
|
10571
10636
|
}
|
|
10572
10637
|
return [];
|
|
10573
10638
|
}
|
|
10574
|
-
function
|
|
10639
|
+
function toMemberArray(arr) {
|
|
10575
10640
|
const result = [];
|
|
10576
10641
|
for (const v of arr) {
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
|
|
10642
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
10643
|
+
result.push(String(v));
|
|
10644
|
+
else if (typeof v === "string" && v !== "")
|
|
10645
|
+
result.push(v);
|
|
10580
10646
|
}
|
|
10581
10647
|
return result;
|
|
10582
10648
|
}
|
|
@@ -11439,9 +11505,15 @@ var SECURITY_PARAMS = {
|
|
|
11439
11505
|
provenance: "guessed"
|
|
11440
11506
|
},
|
|
11441
11507
|
1157: {
|
|
11442
|
-
name: "
|
|
11508
|
+
name: "homeActionTable",
|
|
11443
11509
|
type: "string",
|
|
11444
|
-
provenance: "
|
|
11510
|
+
provenance: "verified",
|
|
11511
|
+
encoding: "base64+json"
|
|
11512
|
+
},
|
|
11513
|
+
1158: {
|
|
11514
|
+
name: "awayActionTable",
|
|
11515
|
+
type: "string",
|
|
11516
|
+
provenance: "verified",
|
|
11445
11517
|
encoding: "base64+json"
|
|
11446
11518
|
},
|
|
11447
11519
|
1159: {
|
|
@@ -15367,7 +15439,7 @@ var PADDED_RESULT_BYTES = 132;
|
|
|
15367
15439
|
var CMD_CAMERA_INFO = 1103;
|
|
15368
15440
|
var CMD_DATABASE_IMAGE = 1308;
|
|
15369
15441
|
var CMD_DATABASE = 1306;
|
|
15370
|
-
var DB_QUERY = { FULL_TABLE: 1e4, COMBINATION_WITH_AI: 10011 };
|
|
15442
|
+
var DB_QUERY = { FULL_TABLE: 1e4, COMBINATION_WITH_AI: 10011, QUERY_LOCAL: 10017 };
|
|
15371
15443
|
var GCM_AAD = Buffer.from("eufy security");
|
|
15372
15444
|
var LEVEL2_SEQ_BASE = 16909056;
|
|
15373
15445
|
function paramReport(payload) {
|
|
@@ -15397,9 +15469,13 @@ function firstTableRows(text2) {
|
|
|
15397
15469
|
} catch {
|
|
15398
15470
|
continue;
|
|
15399
15471
|
}
|
|
15400
|
-
const
|
|
15401
|
-
if (
|
|
15402
|
-
return
|
|
15472
|
+
const reply = parsed;
|
|
15473
|
+
if (typeof reply?.mIntRet === "number" && reply.mIntRet !== 0)
|
|
15474
|
+
return { error: reply.mIntRet };
|
|
15475
|
+
if (reply?.data === "[]")
|
|
15476
|
+
return { rows: [] };
|
|
15477
|
+
if (Array.isArray(reply?.data))
|
|
15478
|
+
return { rows: reply.data };
|
|
15403
15479
|
}
|
|
15404
15480
|
return void 0;
|
|
15405
15481
|
}
|
|
@@ -16021,6 +16097,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16021
16097
|
socket.removeAllListeners("error");
|
|
16022
16098
|
socket.on("error", (error) => this.emit("error", error));
|
|
16023
16099
|
this.socket = socket;
|
|
16100
|
+
this.requestReceiveBuffer(socket);
|
|
16024
16101
|
}
|
|
16025
16102
|
this.connected = true;
|
|
16026
16103
|
this.connectedAtMs = Date.now();
|
|
@@ -16759,6 +16836,29 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16759
16836
|
query: this.fullTableQuery()
|
|
16760
16837
|
});
|
|
16761
16838
|
}
|
|
16839
|
+
/**
|
|
16840
|
+
* Read a date-ranged page of `history_record_info` from the station. The reply's `data` array groups
|
|
16841
|
+
* recording rows with companion tables; each group retains its `table_name` and `payload`.
|
|
16842
|
+
* `startTime` is `"0"` for the newest page or the oldest row's `yyyyMMddHHmmss` cursor for older rows.
|
|
16843
|
+
*/
|
|
16844
|
+
queryRecordPage(opts) {
|
|
16845
|
+
return this.readDatabase("history_record_info", {
|
|
16846
|
+
accountId: opts.accountId,
|
|
16847
|
+
innerCmd: DB_QUERY.QUERY_LOCAL,
|
|
16848
|
+
query: {
|
|
16849
|
+
count: opts.count ?? 20,
|
|
16850
|
+
start_date: opts.startDate,
|
|
16851
|
+
end_date: opts.endDate,
|
|
16852
|
+
start_time: opts.startTime ?? "0",
|
|
16853
|
+
event_type: 0,
|
|
16854
|
+
ai_type: 0,
|
|
16855
|
+
storage_cloud: -1,
|
|
16856
|
+
trigger_type: 0,
|
|
16857
|
+
detection_type: 0,
|
|
16858
|
+
flag: 0
|
|
16859
|
+
}
|
|
16860
|
+
});
|
|
16861
|
+
}
|
|
16762
16862
|
/**
|
|
16763
16863
|
* Query one on-station table and answer its rows, once the reply is whole.
|
|
16764
16864
|
*
|
|
@@ -16766,7 +16866,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16766
16866
|
* several frames whose decrypted text is a fragment of one document, so the fragments are
|
|
16767
16867
|
* accumulated here and scanned after each one. Answers that document's `data` rows. Rejects when
|
|
16768
16868
|
* `signal` aborts, and when `timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}) elapses with no
|
|
16769
|
-
* complete reply
|
|
16869
|
+
* complete reply, and when the station answers a non-zero `mIntRet`.
|
|
16770
16870
|
*
|
|
16771
16871
|
* One read at a time per session: while one is accumulating, every {@link queryDatabase} on the
|
|
16772
16872
|
* session throws, so a second reply cannot land in this buffer.
|
|
@@ -16774,16 +16874,22 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16774
16874
|
async readDatabase(table, opts = {}) {
|
|
16775
16875
|
if (opts.signal?.aborted)
|
|
16776
16876
|
throw new Error("readDatabase: aborted");
|
|
16777
|
-
this.queryDatabase(table, {
|
|
16877
|
+
this.queryDatabase(table, {
|
|
16878
|
+
accountId: opts.accountId,
|
|
16879
|
+
innerCmd: opts.innerCmd,
|
|
16880
|
+
query: opts.query ?? this.fullTableQuery()
|
|
16881
|
+
});
|
|
16778
16882
|
this.dbReadInFlight = true;
|
|
16779
16883
|
try {
|
|
16780
16884
|
return await new Promise((resolve, reject) => {
|
|
16781
16885
|
let text2 = "";
|
|
16782
16886
|
const onChunk = (chunk) => {
|
|
16783
16887
|
text2 += chunk.text;
|
|
16784
|
-
const
|
|
16785
|
-
if (
|
|
16786
|
-
settle(() =>
|
|
16888
|
+
const result = firstTableRows(text2);
|
|
16889
|
+
if (result?.error !== void 0)
|
|
16890
|
+
settle(() => reject(new Error(`readDatabase: ${table} refused: ${result.error}`)));
|
|
16891
|
+
else if (result?.rows)
|
|
16892
|
+
settle(() => resolve(result.rows));
|
|
16787
16893
|
};
|
|
16788
16894
|
const timer = setTimeout(() => settle(() => reject(new Error(`readDatabase: ${this.cfg.stationSn} sent no complete ${table}`))), opts.timeoutMs ?? DB_TABLE_TIMEOUT_MS);
|
|
16789
16895
|
const onAbort = () => settle(() => reject(new Error("readDatabase: aborted")));
|
|
@@ -20844,16 +20950,12 @@ function stationOf(dev) {
|
|
|
20844
20950
|
const parent = typeof raw.parent_sn === "string" && raw.parent_sn && raw.parent_sn !== dev.sn ? raw.parent_sn : void 0;
|
|
20845
20951
|
return parent ?? dev.stationSn ?? resolvedStationSn(raw, dev.sn);
|
|
20846
20952
|
}
|
|
20847
|
-
function statedChannel(dev) {
|
|
20848
|
-
const v = dev.raw?.device_channel;
|
|
20849
|
-
return typeof v === "number" ? v : void 0;
|
|
20850
|
-
}
|
|
20851
20953
|
function stationChannels(devices) {
|
|
20852
20954
|
const out = /* @__PURE__ */ new Map();
|
|
20853
20955
|
const claimants = /* @__PURE__ */ new Map();
|
|
20854
20956
|
for (const d of devices) {
|
|
20855
20957
|
const station = stationOf(d);
|
|
20856
|
-
const stated =
|
|
20958
|
+
const stated = d.channel;
|
|
20857
20959
|
if (d.sn === station || stated === void 0)
|
|
20858
20960
|
continue;
|
|
20859
20961
|
const counts = claimants.get(station) ?? /* @__PURE__ */ new Map();
|
|
@@ -20862,7 +20964,7 @@ function stationChannels(devices) {
|
|
|
20862
20964
|
}
|
|
20863
20965
|
for (const d of devices) {
|
|
20864
20966
|
const station = stationOf(d);
|
|
20865
|
-
const stated =
|
|
20967
|
+
const stated = d.channel;
|
|
20866
20968
|
if (d.sn === station)
|
|
20867
20969
|
out.set(d.sn, { channel: stated ?? 0 });
|
|
20868
20970
|
else if (stated === void 0)
|
|
@@ -22264,7 +22366,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22264
22366
|
* **`set-json-raw` intent** — bare JSON, no envelope: outer P2P cmd = `outerCmd` itself, plaintext
|
|
22265
22367
|
* exactly `{account_id,...data}` (`session.sendRawLevel2` with no wrapper). Reversed from a live
|
|
22266
22368
|
* 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.
|
|
22369
|
+
* (`snoozeTime`); the alarm-delay config (1255, `arming.ts`'s `ARMING_CMD.SET_ALL_ACTION`) reuses
|
|
22268
22370
|
* the same bare-JSON shape.
|
|
22269
22371
|
*/
|
|
22270
22372
|
async sendJsonRaw(sn, outerCmd, data, channel) {
|
|
@@ -22927,6 +23029,7 @@ var RtcPeer = class extends EventEmitter7 {
|
|
|
22927
23029
|
wireTally = /* @__PURE__ */ new Map();
|
|
22928
23030
|
commandOpen = false;
|
|
22929
23031
|
remoteSet = false;
|
|
23032
|
+
remoteMid = HUB_SDP_MID;
|
|
22930
23033
|
handlingOffer = false;
|
|
22931
23034
|
channelsCreated = false;
|
|
22932
23035
|
gatheringDone = false;
|
|
@@ -22947,7 +23050,7 @@ var RtcPeer = class extends EventEmitter7 {
|
|
|
22947
23050
|
const createPeer = this.opts.createPeer ?? await loadNativePeerFactory();
|
|
22948
23051
|
const config = {
|
|
22949
23052
|
iceServers: turnServers(turn),
|
|
22950
|
-
iceTransportPolicy: "relay",
|
|
23053
|
+
iceTransportPolicy: this.opts.iceTransportPolicy ?? "relay",
|
|
22951
23054
|
maxMessageSize: ANKER_MAX_MESSAGE_SIZE,
|
|
22952
23055
|
enableIceTcp: true
|
|
22953
23056
|
};
|
|
@@ -22998,6 +23101,8 @@ var RtcPeer = class extends EventEmitter7 {
|
|
|
22998
23101
|
this.handlingOffer = true;
|
|
22999
23102
|
try {
|
|
23000
23103
|
const offer = forceDtlsRole(offerSdp, "passive");
|
|
23104
|
+
const application = offer.split(/(?=^m=)/m).find((section) => section.startsWith("m=application "));
|
|
23105
|
+
this.remoteMid = application?.match(/^a=mid:([^\r\n]+)/m)?.[1] ?? HUB_SDP_MID;
|
|
23001
23106
|
const answerWait = new Promise((resolve, reject) => {
|
|
23002
23107
|
const timer = setTimeout(() => {
|
|
23003
23108
|
this.localAnswer = void 0;
|
|
@@ -23070,7 +23175,7 @@ var RtcPeer = class extends EventEmitter7 {
|
|
|
23070
23175
|
this.pending.length = 0;
|
|
23071
23176
|
}
|
|
23072
23177
|
acceptsCandidate(candidate) {
|
|
23073
|
-
return iceCandidateType(candidate) === "relay";
|
|
23178
|
+
return this.opts.iceTransportPolicy === "all" || iceCandidateType(candidate) === "relay";
|
|
23074
23179
|
}
|
|
23075
23180
|
createChannels() {
|
|
23076
23181
|
if (!this.pc || this.channelsCreated)
|
|
@@ -23126,7 +23231,7 @@ var RtcPeer = class extends EventEmitter7 {
|
|
|
23126
23231
|
}
|
|
23127
23232
|
addNow(candidate) {
|
|
23128
23233
|
try {
|
|
23129
|
-
this.pc?.addRemoteCandidate(candidate,
|
|
23234
|
+
this.pc?.addRemoteCandidate(candidate, this.remoteMid);
|
|
23130
23235
|
} catch (e) {
|
|
23131
23236
|
this.logger.warn(`[rtc] addRemoteCandidate failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
23132
23237
|
}
|
|
@@ -23303,14 +23408,14 @@ var RtcSignalingClient = class extends EventEmitter8 {
|
|
|
23303
23408
|
this.sendEnvelope(`${this.opts.authToken}_${this.makeMsgId()}`, inner);
|
|
23304
23409
|
}
|
|
23305
23410
|
sendCall() {
|
|
23306
|
-
this.sendSession("scall");
|
|
23411
|
+
this.sendSession(this.opts.signalingMode ?? "scall");
|
|
23307
23412
|
}
|
|
23308
23413
|
sendAck() {
|
|
23309
23414
|
this.sendSession("ack");
|
|
23310
23415
|
}
|
|
23311
|
-
/** The SDP answer,
|
|
23312
|
-
sendInfoSdp(
|
|
23313
|
-
this.sendSession("info", { sdp
|
|
23416
|
+
/** The SDP answer, encoded for the negotiated call format, in an `info`. */
|
|
23417
|
+
sendInfoSdp(sdp) {
|
|
23418
|
+
this.sendSession("info", { sdp });
|
|
23314
23419
|
}
|
|
23315
23420
|
/** Trickle a candidate on the session's channel, like its SDP answer. An empty candidate ends them. */
|
|
23316
23421
|
sendInfoCandidate(candidate) {
|
|
@@ -23438,7 +23543,7 @@ var RtcSession = class extends EventEmitter9 {
|
|
|
23438
23543
|
return this.connected;
|
|
23439
23544
|
}
|
|
23440
23545
|
/**
|
|
23441
|
-
* Start the sequence; resolves once
|
|
23546
|
+
* Start the sequence; resolves once the call is sent. `connected` fires when the channel opens. A session
|
|
23442
23547
|
* closed by the time the sign fetch or the auth completes rejects instead of opening the socket or
|
|
23443
23548
|
* placing the call.
|
|
23444
23549
|
*/
|
|
@@ -23450,7 +23555,7 @@ var RtcSession = class extends EventEmitter9 {
|
|
|
23450
23555
|
await this.waitForAuth();
|
|
23451
23556
|
if (this.closed)
|
|
23452
23557
|
throw new Error("RTC session closed before the call was placed");
|
|
23453
|
-
this.logger.debug(`[rtc] ${this.opts.stationSn} authenticated \u2014 scall`);
|
|
23558
|
+
this.logger.debug(`[rtc] ${this.opts.stationSn} authenticated \u2014 ${this.opts.signalingMode ?? "scall"}`);
|
|
23454
23559
|
this.signaling.sendCall();
|
|
23455
23560
|
}
|
|
23456
23561
|
/** Send one portal packet; false when the command channel isn't open. */
|
|
@@ -23587,7 +23692,7 @@ var RtcSession = class extends EventEmitter9 {
|
|
|
23587
23692
|
offer = sdpText;
|
|
23588
23693
|
}
|
|
23589
23694
|
const answer = await this.peer.handleRemoteOffer(offer);
|
|
23590
|
-
this.signaling.sendInfoSdp(JSON.stringify(sdpToScallJson(answer)));
|
|
23695
|
+
this.signaling.sendInfoSdp(this.opts.signalingMode === "call" ? answer : JSON.stringify(sdpToScallJson(answer)));
|
|
23591
23696
|
this.logger.debug(`[rtc] ${this.opts.stationSn} answered the hub's offer`);
|
|
23592
23697
|
}
|
|
23593
23698
|
};
|
|
@@ -23608,7 +23713,7 @@ var RtcCommandRouter = class {
|
|
|
23608
23713
|
}
|
|
23609
23714
|
async dispatchCommand(route, cmd) {
|
|
23610
23715
|
if (cmd.kind !== "set-payload" && cmd.kind !== "set-json") {
|
|
23611
|
-
throw new Error(`rtc: ${cmd.kind} is not routable over the
|
|
23716
|
+
throw new Error(`rtc: ${cmd.kind} is not routable over the RTC control channel (only set-payload or set-json)`);
|
|
23612
23717
|
}
|
|
23613
23718
|
const identity = this.deps.identity();
|
|
23614
23719
|
if (!identity)
|
|
@@ -23616,7 +23721,7 @@ var RtcCommandRouter = class {
|
|
|
23616
23721
|
const { stationSn } = route;
|
|
23617
23722
|
const adminUserId = route.adminUserId || identity.userId;
|
|
23618
23723
|
const channel = route.attached ? cmd.channel : PORTAL_STATION_CHANNEL;
|
|
23619
|
-
const st = await this.stationSession(
|
|
23724
|
+
const st = await this.stationSession(route, adminUserId, identity);
|
|
23620
23725
|
const outerCmd = cmd.kind === "set-json" ? PORTAL_CMD_CONTROL_PAYLOAD : PORTAL_CMD_SET_PAYLOAD;
|
|
23621
23726
|
const innerCmd = cmd.kind === "set-json" ? cmd.param : cmd.cmd;
|
|
23622
23727
|
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 +23782,8 @@ var RtcCommandRouter = class {
|
|
|
23677
23782
|
}
|
|
23678
23783
|
});
|
|
23679
23784
|
}
|
|
23680
|
-
async stationSession(
|
|
23785
|
+
async stationSession(route, adminUserId, identity) {
|
|
23786
|
+
const sn = route.stationSn;
|
|
23681
23787
|
const existing = this.sessions.get(sn);
|
|
23682
23788
|
if (existing) {
|
|
23683
23789
|
await existing.ready;
|
|
@@ -23695,7 +23801,8 @@ var RtcCommandRouter = class {
|
|
|
23695
23801
|
shard: this.deps.shard(),
|
|
23696
23802
|
country: this.deps.country ?? "US",
|
|
23697
23803
|
logger: this.deps.logger,
|
|
23698
|
-
|
|
23804
|
+
signalingMode: route.signalingMode,
|
|
23805
|
+
peer: { logger: this.deps.logger, iceTransportPolicy: route.iceTransportPolicy }
|
|
23699
23806
|
});
|
|
23700
23807
|
const ready = this.bringUp(sn, session);
|
|
23701
23808
|
const st = { session, seg: new SegmentCounter(), ready, queue: Promise.resolve() };
|
|
@@ -26556,6 +26663,7 @@ var DeviceRegistry = class {
|
|
|
26556
26663
|
name: recordName(raw, deviceClass),
|
|
26557
26664
|
model: raw.device_model,
|
|
26558
26665
|
stationSn: resolvedStationSn(raw, raw.device_sn),
|
|
26666
|
+
...typeof raw.device_channel === "number" ? { channel: raw.device_channel } : {},
|
|
26559
26667
|
p2pDid: raw.p2p_did,
|
|
26560
26668
|
params,
|
|
26561
26669
|
paramUpdatedAt,
|
|
@@ -27908,9 +28016,10 @@ var EufyMega = class extends EventEmitter12 {
|
|
|
27908
28016
|
* The `eufy_life` DP writes (smart lights) are secure-MQTT-only. `aiot-dp` routes to either the
|
|
27909
28017
|
* Anker AIoT MQTT stack or the legacy Tuya REST router depending on the device's category
|
|
27910
28018
|
* (`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
|
|
28019
|
+
* kind and stays transport-agnostic; only the facade sees both sides and decides here. A T9000 or T8N00
|
|
27912
28020
|
* 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.
|
|
28021
|
+
* unless its channel resolves back to it on the station. T8N00 uses full SDP and admits direct and
|
|
28022
|
+
* relay candidates; T9000 retains compact SDP and relay-only candidates. Everything else is P2P.
|
|
27914
28023
|
*/
|
|
27915
28024
|
routeCommand(sn, cmd) {
|
|
27916
28025
|
if (cmd.kind === "ff09-actuate" || cmd.kind === "ff09-autolock" || cmd.kind === "ff09-setting-toggle") {
|
|
@@ -27936,13 +28045,16 @@ var EufyMega = class extends EventEmitter12 {
|
|
|
27936
28045
|
const station = devices.find((d) => d.sn === stationSn);
|
|
27937
28046
|
const stationRaw = station?.raw ?? {};
|
|
27938
28047
|
const deviceType = typeof stationRaw.device_type === "number" ? stationRaw.device_type : void 0;
|
|
27939
|
-
|
|
28048
|
+
const family = { deviceType, model: station?.model };
|
|
28049
|
+
const native = isNvrT8N00(family);
|
|
28050
|
+
if (target && station && (isStation9000(family) || native)) {
|
|
27940
28051
|
const attached = stationSn !== sn;
|
|
27941
28052
|
if (attached && this.registry.serialForFrame(stationSn, cmd.channel) !== sn)
|
|
27942
28053
|
return Promise.reject(new Error("RTC command requires an unambiguous attached-device channel"));
|
|
27943
28054
|
const member = stationRaw.member?.admin_user_id;
|
|
27944
28055
|
const adminUserId = typeof member === "string" && member ? member : void 0;
|
|
27945
|
-
|
|
28056
|
+
const base = { stationSn, adminUserId, attached };
|
|
28057
|
+
return this.rtc.dispatchCommand(native ? { ...base, signalingMode: "call", iceTransportPolicy: "all" } : base, cmd);
|
|
27946
28058
|
}
|
|
27947
28059
|
return this.p2p.dispatchCommand(sn, cmd);
|
|
27948
28060
|
}
|