@mega-yfue/eufy-sdk 0.5.0-beta.20 → 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/contracts.d.ts +4 -3
- package/dist/core/types.d.ts +2 -0
- package/dist/index.js +164 -56
- 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/p2p/recording-download.d.ts +4 -3
- 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/contracts.d.ts
CHANGED
|
@@ -171,9 +171,10 @@ export declare class RecordingDownloadError extends Error {
|
|
|
171
171
|
* A recording downloaded from the station, as elementary streams.
|
|
172
172
|
*
|
|
173
173
|
* `video` is Annex-B H.264 in decode order, `audio` AAC-LC 16 kHz mono in ADTS framing (absent when the
|
|
174
|
-
* recording carried none that could be decoded). `frames` counts the distinct video
|
|
175
|
-
* `missingFrames`
|
|
176
|
-
*
|
|
174
|
+
* recording carried none that could be decoded). `frames` counts the distinct video sequences emitted.
|
|
175
|
+
* `missingFrames` counts gaps between received sequences and received sequences that yielded no usable
|
|
176
|
+
* picture, including at either end. `durationMs` spans the first to the last emitted picture by the
|
|
177
|
+
* camera's own timestamps; `fps` is derived from those stamps.
|
|
177
178
|
*/
|
|
178
179
|
export interface RecordingDownload {
|
|
179
180
|
video: Buffer;
|
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")));
|
|
@@ -18670,9 +18768,11 @@ function decodeRecording(frames, eccPrivateKeyHex) {
|
|
|
18670
18768
|
if (eccPrivateKey.length !== 32)
|
|
18671
18769
|
throw new RecordingDownloadError("undecodable", "the cipher key is not a P-256 key");
|
|
18672
18770
|
const decoder = new VideoFrameDecoder(eccPrivateKey);
|
|
18771
|
+
const assembler = new AccessUnitAssembler();
|
|
18673
18772
|
const video = [];
|
|
18674
18773
|
const audio = [];
|
|
18675
18774
|
const numbers = /* @__PURE__ */ new Set();
|
|
18775
|
+
const receivedNumbers = /* @__PURE__ */ new Set();
|
|
18676
18776
|
let firstStamp;
|
|
18677
18777
|
let lastStamp;
|
|
18678
18778
|
for (const { commandId, raw } of frames) {
|
|
@@ -18680,14 +18780,15 @@ function decodeRecording(frames, eccPrivateKeyHex) {
|
|
|
18680
18780
|
const header = parseVideoFrameHeader(raw);
|
|
18681
18781
|
if (!header)
|
|
18682
18782
|
continue;
|
|
18683
|
-
|
|
18684
|
-
|
|
18685
|
-
|
|
18686
|
-
|
|
18687
|
-
|
|
18688
|
-
|
|
18689
|
-
|
|
18690
|
-
|
|
18783
|
+
receivedNumbers.add(header.sequence);
|
|
18784
|
+
const units = assembler.push(raw, (payload) => header.keyframe ? decoder.decodeFrame(payload)?.h264 : payload.length >= VIDEO_HEADER_LEN2 + header.payloadLength ? payload.subarray(VIDEO_HEADER_LEN2, VIDEO_HEADER_LEN2 + header.payloadLength) : void 0);
|
|
18785
|
+
for (const unit of units) {
|
|
18786
|
+
video.push(unit.data);
|
|
18787
|
+
numbers.add(header.sequence);
|
|
18788
|
+
const stamp = raw.readUIntLE(14, 6);
|
|
18789
|
+
firstStamp ??= stamp;
|
|
18790
|
+
lastStamp = stamp;
|
|
18791
|
+
}
|
|
18691
18792
|
} else if (commandId === CommandType.CMD_AUDIO_FRAME) {
|
|
18692
18793
|
const mediaKey = decoder.currentMediaKey;
|
|
18693
18794
|
if (!mediaKey || raw.length < AUDIO_BODY_START || raw[5] !== AUDIO_CODEC_AAC_LC)
|
|
@@ -18703,9 +18804,10 @@ function decodeRecording(frames, eccPrivateKeyHex) {
|
|
|
18703
18804
|
if (!video.length)
|
|
18704
18805
|
throw new RecordingDownloadError("undecodable", "no video frame of the recording decoded");
|
|
18705
18806
|
const ordered = [...numbers];
|
|
18706
|
-
|
|
18707
|
-
|
|
18708
|
-
|
|
18807
|
+
const received = [...receivedNumbers];
|
|
18808
|
+
let missingFrames = receivedNumbers.size - numbers.size;
|
|
18809
|
+
for (let i = 1; i < received.length; i++) {
|
|
18810
|
+
const step = received[i] - received[i - 1] & 65535;
|
|
18709
18811
|
if (step > 1 && step < 32768)
|
|
18710
18812
|
missingFrames += step - 1;
|
|
18711
18813
|
}
|
|
@@ -20840,16 +20942,12 @@ function stationOf(dev) {
|
|
|
20840
20942
|
const parent = typeof raw.parent_sn === "string" && raw.parent_sn && raw.parent_sn !== dev.sn ? raw.parent_sn : void 0;
|
|
20841
20943
|
return parent ?? dev.stationSn ?? resolvedStationSn(raw, dev.sn);
|
|
20842
20944
|
}
|
|
20843
|
-
function statedChannel(dev) {
|
|
20844
|
-
const v = dev.raw?.device_channel;
|
|
20845
|
-
return typeof v === "number" ? v : void 0;
|
|
20846
|
-
}
|
|
20847
20945
|
function stationChannels(devices) {
|
|
20848
20946
|
const out = /* @__PURE__ */ new Map();
|
|
20849
20947
|
const claimants = /* @__PURE__ */ new Map();
|
|
20850
20948
|
for (const d of devices) {
|
|
20851
20949
|
const station = stationOf(d);
|
|
20852
|
-
const stated =
|
|
20950
|
+
const stated = d.channel;
|
|
20853
20951
|
if (d.sn === station || stated === void 0)
|
|
20854
20952
|
continue;
|
|
20855
20953
|
const counts = claimants.get(station) ?? /* @__PURE__ */ new Map();
|
|
@@ -20858,7 +20956,7 @@ function stationChannels(devices) {
|
|
|
20858
20956
|
}
|
|
20859
20957
|
for (const d of devices) {
|
|
20860
20958
|
const station = stationOf(d);
|
|
20861
|
-
const stated =
|
|
20959
|
+
const stated = d.channel;
|
|
20862
20960
|
if (d.sn === station)
|
|
20863
20961
|
out.set(d.sn, { channel: stated ?? 0 });
|
|
20864
20962
|
else if (stated === void 0)
|
|
@@ -22260,7 +22358,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22260
22358
|
* **`set-json-raw` intent** — bare JSON, no envelope: outer P2P cmd = `outerCmd` itself, plaintext
|
|
22261
22359
|
* exactly `{account_id,...data}` (`session.sendRawLevel2` with no wrapper). Reversed from a live
|
|
22262
22360
|
* capture of the app's own SET_SNOOZE_TIME (1271) frame — see `param-dictionary.ts`'s `1271` entry
|
|
22263
|
-
* (`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
|
|
22264
22362
|
* the same bare-JSON shape.
|
|
22265
22363
|
*/
|
|
22266
22364
|
async sendJsonRaw(sn, outerCmd, data, channel) {
|
|
@@ -22923,6 +23021,7 @@ var RtcPeer = class extends EventEmitter7 {
|
|
|
22923
23021
|
wireTally = /* @__PURE__ */ new Map();
|
|
22924
23022
|
commandOpen = false;
|
|
22925
23023
|
remoteSet = false;
|
|
23024
|
+
remoteMid = HUB_SDP_MID;
|
|
22926
23025
|
handlingOffer = false;
|
|
22927
23026
|
channelsCreated = false;
|
|
22928
23027
|
gatheringDone = false;
|
|
@@ -22943,7 +23042,7 @@ var RtcPeer = class extends EventEmitter7 {
|
|
|
22943
23042
|
const createPeer = this.opts.createPeer ?? await loadNativePeerFactory();
|
|
22944
23043
|
const config = {
|
|
22945
23044
|
iceServers: turnServers(turn),
|
|
22946
|
-
iceTransportPolicy: "relay",
|
|
23045
|
+
iceTransportPolicy: this.opts.iceTransportPolicy ?? "relay",
|
|
22947
23046
|
maxMessageSize: ANKER_MAX_MESSAGE_SIZE,
|
|
22948
23047
|
enableIceTcp: true
|
|
22949
23048
|
};
|
|
@@ -22994,6 +23093,8 @@ var RtcPeer = class extends EventEmitter7 {
|
|
|
22994
23093
|
this.handlingOffer = true;
|
|
22995
23094
|
try {
|
|
22996
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;
|
|
22997
23098
|
const answerWait = new Promise((resolve, reject) => {
|
|
22998
23099
|
const timer = setTimeout(() => {
|
|
22999
23100
|
this.localAnswer = void 0;
|
|
@@ -23066,7 +23167,7 @@ var RtcPeer = class extends EventEmitter7 {
|
|
|
23066
23167
|
this.pending.length = 0;
|
|
23067
23168
|
}
|
|
23068
23169
|
acceptsCandidate(candidate) {
|
|
23069
|
-
return iceCandidateType(candidate) === "relay";
|
|
23170
|
+
return this.opts.iceTransportPolicy === "all" || iceCandidateType(candidate) === "relay";
|
|
23070
23171
|
}
|
|
23071
23172
|
createChannels() {
|
|
23072
23173
|
if (!this.pc || this.channelsCreated)
|
|
@@ -23122,7 +23223,7 @@ var RtcPeer = class extends EventEmitter7 {
|
|
|
23122
23223
|
}
|
|
23123
23224
|
addNow(candidate) {
|
|
23124
23225
|
try {
|
|
23125
|
-
this.pc?.addRemoteCandidate(candidate,
|
|
23226
|
+
this.pc?.addRemoteCandidate(candidate, this.remoteMid);
|
|
23126
23227
|
} catch (e) {
|
|
23127
23228
|
this.logger.warn(`[rtc] addRemoteCandidate failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
23128
23229
|
}
|
|
@@ -23299,14 +23400,14 @@ var RtcSignalingClient = class extends EventEmitter8 {
|
|
|
23299
23400
|
this.sendEnvelope(`${this.opts.authToken}_${this.makeMsgId()}`, inner);
|
|
23300
23401
|
}
|
|
23301
23402
|
sendCall() {
|
|
23302
|
-
this.sendSession("scall");
|
|
23403
|
+
this.sendSession(this.opts.signalingMode ?? "scall");
|
|
23303
23404
|
}
|
|
23304
23405
|
sendAck() {
|
|
23305
23406
|
this.sendSession("ack");
|
|
23306
23407
|
}
|
|
23307
|
-
/** The SDP answer,
|
|
23308
|
-
sendInfoSdp(
|
|
23309
|
-
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 });
|
|
23310
23411
|
}
|
|
23311
23412
|
/** Trickle a candidate on the session's channel, like its SDP answer. An empty candidate ends them. */
|
|
23312
23413
|
sendInfoCandidate(candidate) {
|
|
@@ -23434,7 +23535,7 @@ var RtcSession = class extends EventEmitter9 {
|
|
|
23434
23535
|
return this.connected;
|
|
23435
23536
|
}
|
|
23436
23537
|
/**
|
|
23437
|
-
* Start the sequence; resolves once
|
|
23538
|
+
* Start the sequence; resolves once the call is sent. `connected` fires when the channel opens. A session
|
|
23438
23539
|
* closed by the time the sign fetch or the auth completes rejects instead of opening the socket or
|
|
23439
23540
|
* placing the call.
|
|
23440
23541
|
*/
|
|
@@ -23446,7 +23547,7 @@ var RtcSession = class extends EventEmitter9 {
|
|
|
23446
23547
|
await this.waitForAuth();
|
|
23447
23548
|
if (this.closed)
|
|
23448
23549
|
throw new Error("RTC session closed before the call was placed");
|
|
23449
|
-
this.logger.debug(`[rtc] ${this.opts.stationSn} authenticated \u2014 scall`);
|
|
23550
|
+
this.logger.debug(`[rtc] ${this.opts.stationSn} authenticated \u2014 ${this.opts.signalingMode ?? "scall"}`);
|
|
23450
23551
|
this.signaling.sendCall();
|
|
23451
23552
|
}
|
|
23452
23553
|
/** Send one portal packet; false when the command channel isn't open. */
|
|
@@ -23583,7 +23684,7 @@ var RtcSession = class extends EventEmitter9 {
|
|
|
23583
23684
|
offer = sdpText;
|
|
23584
23685
|
}
|
|
23585
23686
|
const answer = await this.peer.handleRemoteOffer(offer);
|
|
23586
|
-
this.signaling.sendInfoSdp(JSON.stringify(sdpToScallJson(answer)));
|
|
23687
|
+
this.signaling.sendInfoSdp(this.opts.signalingMode === "call" ? answer : JSON.stringify(sdpToScallJson(answer)));
|
|
23587
23688
|
this.logger.debug(`[rtc] ${this.opts.stationSn} answered the hub's offer`);
|
|
23588
23689
|
}
|
|
23589
23690
|
};
|
|
@@ -23604,7 +23705,7 @@ var RtcCommandRouter = class {
|
|
|
23604
23705
|
}
|
|
23605
23706
|
async dispatchCommand(route, cmd) {
|
|
23606
23707
|
if (cmd.kind !== "set-payload" && cmd.kind !== "set-json") {
|
|
23607
|
-
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)`);
|
|
23608
23709
|
}
|
|
23609
23710
|
const identity = this.deps.identity();
|
|
23610
23711
|
if (!identity)
|
|
@@ -23612,7 +23713,7 @@ var RtcCommandRouter = class {
|
|
|
23612
23713
|
const { stationSn } = route;
|
|
23613
23714
|
const adminUserId = route.adminUserId || identity.userId;
|
|
23614
23715
|
const channel = route.attached ? cmd.channel : PORTAL_STATION_CHANNEL;
|
|
23615
|
-
const st = await this.stationSession(
|
|
23716
|
+
const st = await this.stationSession(route, adminUserId, identity);
|
|
23616
23717
|
const outerCmd = cmd.kind === "set-json" ? PORTAL_CMD_CONTROL_PAYLOAD : PORTAL_CMD_SET_PAYLOAD;
|
|
23617
23718
|
const innerCmd = cmd.kind === "set-json" ? cmd.param : cmd.cmd;
|
|
23618
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 };
|
|
@@ -23673,7 +23774,8 @@ var RtcCommandRouter = class {
|
|
|
23673
23774
|
}
|
|
23674
23775
|
});
|
|
23675
23776
|
}
|
|
23676
|
-
async stationSession(
|
|
23777
|
+
async stationSession(route, adminUserId, identity) {
|
|
23778
|
+
const sn = route.stationSn;
|
|
23677
23779
|
const existing = this.sessions.get(sn);
|
|
23678
23780
|
if (existing) {
|
|
23679
23781
|
await existing.ready;
|
|
@@ -23691,7 +23793,8 @@ var RtcCommandRouter = class {
|
|
|
23691
23793
|
shard: this.deps.shard(),
|
|
23692
23794
|
country: this.deps.country ?? "US",
|
|
23693
23795
|
logger: this.deps.logger,
|
|
23694
|
-
|
|
23796
|
+
signalingMode: route.signalingMode,
|
|
23797
|
+
peer: { logger: this.deps.logger, iceTransportPolicy: route.iceTransportPolicy }
|
|
23695
23798
|
});
|
|
23696
23799
|
const ready = this.bringUp(sn, session);
|
|
23697
23800
|
const st = { session, seg: new SegmentCounter(), ready, queue: Promise.resolve() };
|
|
@@ -26552,6 +26655,7 @@ var DeviceRegistry = class {
|
|
|
26552
26655
|
name: recordName(raw, deviceClass),
|
|
26553
26656
|
model: raw.device_model,
|
|
26554
26657
|
stationSn: resolvedStationSn(raw, raw.device_sn),
|
|
26658
|
+
...typeof raw.device_channel === "number" ? { channel: raw.device_channel } : {},
|
|
26555
26659
|
p2pDid: raw.p2p_did,
|
|
26556
26660
|
params,
|
|
26557
26661
|
paramUpdatedAt,
|
|
@@ -27904,9 +28008,10 @@ var EufyMega = class extends EventEmitter12 {
|
|
|
27904
28008
|
* The `eufy_life` DP writes (smart lights) are secure-MQTT-only. `aiot-dp` routes to either the
|
|
27905
28009
|
* Anker AIoT MQTT stack or the legacy Tuya REST router depending on the device's category
|
|
27906
28010
|
* (`eufy_home_tuya` → Tuya, everything else → MQTT). The capability layer emits a single `aiot-dp`
|
|
27907
|
-
* 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
|
|
27908
28012
|
* station and the devices attached to it go over RTC, with an attached device's command refused
|
|
27909
|
-
* 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.
|
|
27910
28015
|
*/
|
|
27911
28016
|
routeCommand(sn, cmd) {
|
|
27912
28017
|
if (cmd.kind === "ff09-actuate" || cmd.kind === "ff09-autolock" || cmd.kind === "ff09-setting-toggle") {
|
|
@@ -27932,13 +28037,16 @@ var EufyMega = class extends EventEmitter12 {
|
|
|
27932
28037
|
const station = devices.find((d) => d.sn === stationSn);
|
|
27933
28038
|
const stationRaw = station?.raw ?? {};
|
|
27934
28039
|
const deviceType = typeof stationRaw.device_type === "number" ? stationRaw.device_type : void 0;
|
|
27935
|
-
|
|
28040
|
+
const family = { deviceType, model: station?.model };
|
|
28041
|
+
const native = isNvrT8N00(family);
|
|
28042
|
+
if (target && station && (isStation9000(family) || native)) {
|
|
27936
28043
|
const attached = stationSn !== sn;
|
|
27937
28044
|
if (attached && this.registry.serialForFrame(stationSn, cmd.channel) !== sn)
|
|
27938
28045
|
return Promise.reject(new Error("RTC command requires an unambiguous attached-device channel"));
|
|
27939
28046
|
const member = stationRaw.member?.admin_user_id;
|
|
27940
28047
|
const adminUserId = typeof member === "string" && member ? member : void 0;
|
|
27941
|
-
|
|
28048
|
+
const base = { stationSn, adminUserId, attached };
|
|
28049
|
+
return this.rtc.dispatchCommand(native ? { ...base, signalingMode: "call", iceTransportPolicy: "all" } : base, cmd);
|
|
27942
28050
|
}
|
|
27943
28051
|
return this.p2p.dispatchCommand(sn, cmd);
|
|
27944
28052
|
}
|