@mega-yfue/eufy-sdk 0.4.0-beta.0 → 0.4.0-beta.10
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 +4 -1
- package/dist/core/contracts.d.ts +2 -2
- package/dist/index.js +106 -42
- package/dist/index.js.map +2 -2
- package/dist/transport/mqtt/secure-mqtt.d.ts +20 -13
- package/dist/transport/p2p/p2p-session.d.ts +21 -2
- package/dist/transport/p2p/shared-live-source.d.ts +1 -1
- package/package.json +3 -3
|
@@ -871,7 +871,10 @@ export declare class EufyMega extends EventEmitter {
|
|
|
871
871
|
* one — so this never overwrites a channel a later login already brought up.
|
|
872
872
|
*/
|
|
873
873
|
private startPush;
|
|
874
|
-
/**
|
|
874
|
+
/**
|
|
875
|
+
* Admit only exact, account-known devices with resolved snapshot evidence into the passive store; a
|
|
876
|
+
* station-only candidate is admitted under the event's device serial.
|
|
877
|
+
*/
|
|
875
878
|
private observeStoredImage;
|
|
876
879
|
/**
|
|
877
880
|
* Tear down every realtime channel: close the secure-MQTT transport, all P2P sessions, and the FCM
|
package/dist/core/contracts.d.ts
CHANGED
|
@@ -684,8 +684,8 @@ export interface SharedSourceHints {
|
|
|
684
684
|
*/
|
|
685
685
|
powered?: "wired" | "battery";
|
|
686
686
|
/**
|
|
687
|
-
* Seconds of already-captured media the pull retains for a later drain; `0`, absent, non-finite
|
|
688
|
-
* negative
|
|
687
|
+
* Seconds of already-captured media the pull retains for a later drain; `0`, absent, non-finite,
|
|
688
|
+
* negative, and windows longer than 2,147,483,647 ms retain none.
|
|
689
689
|
*
|
|
690
690
|
* Retention costs memory on every frame and only a caller knows whether anything will drain it, so it
|
|
691
691
|
* is never assumed. Media can only be retained while the pull is running, so a window answers "what did
|
package/dist/index.js
CHANGED
|
@@ -2125,6 +2125,10 @@ function solixUserTopics(appName, userId) {
|
|
|
2125
2125
|
|
|
2126
2126
|
// dist/transport/mqtt/secure-mqtt.js
|
|
2127
2127
|
var SUBACK_FAILURE = 128;
|
|
2128
|
+
function isRefusal(err) {
|
|
2129
|
+
const granted = err?.packet?.granted;
|
|
2130
|
+
return Array.isArray(granted) && granted.some((g) => typeof g === "number" && (g & SUBACK_FAILURE) !== 0);
|
|
2131
|
+
}
|
|
2128
2132
|
function isNotAuthorized(err) {
|
|
2129
2133
|
return err instanceof Error && /connection refused/i.test(err.message) && /not authori[sz]ed/i.test(err.message);
|
|
2130
2134
|
}
|
|
@@ -2227,19 +2231,16 @@ var SecureMqtt = class extends EventEmitter {
|
|
|
2227
2231
|
}
|
|
2228
2232
|
/**
|
|
2229
2233
|
* Subscribe every inbound leg this device's line uses (see `topics.ts` — one `/res` for most lines,
|
|
2230
|
-
* four topics for `eufy_life`).
|
|
2234
|
+
* four topics for `eufy_life` and the clean line).
|
|
2231
2235
|
*
|
|
2232
|
-
* The grants are INSPECTED, not assumed: AWS IoT
|
|
2233
|
-
* SUBACK_FAILURE
|
|
2234
|
-
*
|
|
2235
|
-
*
|
|
2236
|
-
* line that grants its state channel but refuses (say) the OTA leg still works.
|
|
2236
|
+
* The grants are INSPECTED, not assumed: AWS IoT refuses a policy-denied filter with a
|
|
2237
|
+
* {@link SUBACK_FAILURE} grant rather than failing the connection. A denied topic is reported via
|
|
2238
|
+
* `error` naming the credential scope; only an all-denied device throws, so a line that grants its
|
|
2239
|
+
* state channel but refuses (say) a business leg still works.
|
|
2237
2240
|
*/
|
|
2238
2241
|
async subscribeDevice(device) {
|
|
2239
|
-
if (!this.client)
|
|
2240
|
-
throw new Error("SecureMqtt not connected");
|
|
2241
2242
|
const topics = [...subscribeTopics(device)];
|
|
2242
|
-
const { denied } =
|
|
2243
|
+
const { denied } = await this.subscribeEach(topics);
|
|
2243
2244
|
const scope = this.o.credentials.app_name ?? "default";
|
|
2244
2245
|
if (denied.length === topics.length) {
|
|
2245
2246
|
throw new Error(`subscribe ${device.sn}: every topic denied on credential scope "${scope}" \u2014 this line's topics are granted to a different scope (${denied.join(", ")})`);
|
|
@@ -2250,25 +2251,43 @@ var SecureMqtt = class extends EventEmitter {
|
|
|
2250
2251
|
}
|
|
2251
2252
|
/**
|
|
2252
2253
|
* Subscribe to explicit topic filters, returning the topics that were granted. A scope-denied filter
|
|
2253
|
-
*
|
|
2254
|
-
*
|
|
2255
|
-
*
|
|
2254
|
+
* is dropped from the result instead of throwing — callers that need every leg check the returned
|
|
2255
|
+
* list. Used by lines whose topic vocabulary isn't the eufy `subscribeTopics` shape (e.g. Anker Solix
|
|
2256
|
+
* `dt/{app}/{pn}/{sn}`).
|
|
2256
2257
|
*/
|
|
2257
2258
|
async subscribe(topics) {
|
|
2258
|
-
|
|
2259
|
-
throw new Error("SecureMqtt not connected");
|
|
2260
|
-
return this.partitionGrants(await this.client.subscribeAsync(topics, { qos: 1 })).granted;
|
|
2259
|
+
return (await this.subscribeEach(topics)).granted;
|
|
2261
2260
|
}
|
|
2262
2261
|
/**
|
|
2263
|
-
*
|
|
2264
|
-
*
|
|
2265
|
-
*
|
|
2262
|
+
* Subscribe each filter in a SUBSCRIBE of its own and split the outcome into granted vs refused.
|
|
2263
|
+
*
|
|
2264
|
+
* One request per filter because the MQTT engine treats a SUBACK as all-or-nothing: one refused grant
|
|
2265
|
+
* rejects the whole request, and it forgets EVERY filter of that request for resubscription after a
|
|
2266
|
+
* reconnect — so a batch holding one denied leg would lose the granted legs on the next drop. Alone,
|
|
2267
|
+
* a refused filter costs only itself. A rejection that carries no SUBACK is a transport failure and
|
|
2268
|
+
* is thrown.
|
|
2269
|
+
*
|
|
2270
|
+
* The cost is one SUBSCRIBE and one SUBACK per filter instead of one per device — four for a
|
|
2271
|
+
* four-leg line. They are sent concurrently, so the wall-clock cost is one round trip. Folding them
|
|
2272
|
+
* back into one request brings back the lost resubscription.
|
|
2266
2273
|
*/
|
|
2267
|
-
|
|
2274
|
+
async subscribeEach(topics) {
|
|
2275
|
+
const client = this.client;
|
|
2276
|
+
if (!client)
|
|
2277
|
+
throw new Error("SecureMqtt not connected");
|
|
2278
|
+
const outcomes = await Promise.allSettled(topics.map((topic) => client.subscribeAsync(topic, { qos: 1 })));
|
|
2268
2279
|
const granted = [];
|
|
2269
2280
|
const denied = [];
|
|
2270
|
-
|
|
2271
|
-
|
|
2281
|
+
outcomes.forEach((outcome, i) => {
|
|
2282
|
+
const topic = topics[i];
|
|
2283
|
+
if (outcome.status === "fulfilled") {
|
|
2284
|
+
granted.push(topic);
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
if (!isRefusal(outcome.reason))
|
|
2288
|
+
throw outcome.reason;
|
|
2289
|
+
denied.push(topic);
|
|
2290
|
+
});
|
|
2272
2291
|
return { granted, denied };
|
|
2273
2292
|
}
|
|
2274
2293
|
/**
|
|
@@ -4326,7 +4345,7 @@ function decodePowerSource(raw) {
|
|
|
4326
4345
|
function recordSetting(param, value, ctx) {
|
|
4327
4346
|
return setStationScalar(param, value, ctx.channel);
|
|
4328
4347
|
}
|
|
4329
|
-
var MAINS_CAMERA_MODELS = ["T8425", "T8419", "T8410", "T84A1"];
|
|
4348
|
+
var MAINS_CAMERA_MODELS = ["T8425", "T8419", "T8410", "T84A1", "T8423"];
|
|
4330
4349
|
function cameraPowerTier(model, capabilities) {
|
|
4331
4350
|
return capabilities.has("battery") && notMainsCamera({ model }) ? "battery" : "wired";
|
|
4332
4351
|
}
|
|
@@ -15159,6 +15178,7 @@ var LOCAL_LOOKUP_PORT = 32108;
|
|
|
15159
15178
|
var HEARTBEAT_MS = 5e3;
|
|
15160
15179
|
var PATH_SILENCE_MS = HEARTBEAT_MS * 3;
|
|
15161
15180
|
var LOOKUP_RETRY_MS = 1e3;
|
|
15181
|
+
var RECEIVE_BUFFER_BYTES = 4 * 1024 * 1024;
|
|
15162
15182
|
var CONNECT_TIMEOUT_MS = 15e3;
|
|
15163
15183
|
var STATION_CHANNEL3 = 255;
|
|
15164
15184
|
var CMD_GATEWAYINFO = 1100;
|
|
@@ -15188,6 +15208,7 @@ var REORDER_MAX_DATAGRAMS = 128;
|
|
|
15188
15208
|
var MAX_TRACED_DATAGRAM_GAPS = 8;
|
|
15189
15209
|
var CMD_SET_PAYLOAD = 1350;
|
|
15190
15210
|
var CMD_NOTIFY_PAYLOAD = 1351;
|
|
15211
|
+
var PADDED_RESULT_BYTES = 132;
|
|
15191
15212
|
var CMD_CAMERA_INFO = 1103;
|
|
15192
15213
|
var CMD_DATABASE_IMAGE = 1308;
|
|
15193
15214
|
var CMD_DATABASE = 1306;
|
|
@@ -15213,6 +15234,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15213
15234
|
connected = false;
|
|
15214
15235
|
connecting = false;
|
|
15215
15236
|
closed = false;
|
|
15237
|
+
/** Whether a short receive buffer has been reported, so a reconnect does not repeat the same warning. */
|
|
15238
|
+
receiveBufferReported = false;
|
|
15216
15239
|
connectAddress;
|
|
15217
15240
|
seqNumber = 0;
|
|
15218
15241
|
/**
|
|
@@ -15252,7 +15275,10 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15252
15275
|
cloudLookup;
|
|
15253
15276
|
/** Local outbound IPv4 reported inside LOOKUP_WITH_KEY requests. */
|
|
15254
15277
|
selfHost;
|
|
15255
|
-
/**
|
|
15278
|
+
/**
|
|
15279
|
+
* In-flight multi-datagram frame per data channel (see reassemble): the payload gathered so far under its
|
|
15280
|
+
* parsed header, or, without a header, the start of a frame header cut by the datagram boundary.
|
|
15281
|
+
*/
|
|
15256
15282
|
pendingByDataType = /* @__PURE__ */ new Map();
|
|
15257
15283
|
/** Last delivered datagram sequence number per data type. */
|
|
15258
15284
|
lastSeqByType = /* @__PURE__ */ new Map();
|
|
@@ -15532,6 +15558,26 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15532
15558
|
get isConnected() {
|
|
15533
15559
|
return this.connected;
|
|
15534
15560
|
}
|
|
15561
|
+
/**
|
|
15562
|
+
* Ask the OS for {@link RECEIVE_BUFFER_BYTES} on a bound socket, and warn once per session when it grants
|
|
15563
|
+
* less or refuses.
|
|
15564
|
+
*
|
|
15565
|
+
* The request is made here rather than through `createSocket`'s `recvBufferSize`: Node applies that option
|
|
15566
|
+
* inside the bind callback, where a refusal is thrown out of reach of this session and ends the process.
|
|
15567
|
+
*/
|
|
15568
|
+
requestReceiveBuffer(socket) {
|
|
15569
|
+
let granted;
|
|
15570
|
+
try {
|
|
15571
|
+
socket.setRecvBufferSize(RECEIVE_BUFFER_BYTES);
|
|
15572
|
+
granted = socket.getRecvBufferSize();
|
|
15573
|
+
} catch {
|
|
15574
|
+
granted = 0;
|
|
15575
|
+
}
|
|
15576
|
+
if (granted >= RECEIVE_BUFFER_BYTES || this.receiveBufferReported)
|
|
15577
|
+
return;
|
|
15578
|
+
this.receiveBufferReported = true;
|
|
15579
|
+
this.logger.warn(`[p2p] ${this.cfg.stationSn} UDP receive buffer below the ${RECEIVE_BUFFER_BYTES} bytes requested` + (granted > 0 ? ` (granted ${granted})` : " (request refused)") + `; live video can lose keyframes. Raise the OS limit (net.core.rmem_max on Linux, kern.ipc.maxsockbuf on BSD) to at least ${RECEIVE_BUFFER_BYTES}.`);
|
|
15580
|
+
}
|
|
15535
15581
|
/** Open the socket and start the lookup → hole-punch handshake. */
|
|
15536
15582
|
async connect() {
|
|
15537
15583
|
if (this.connecting || this.connected)
|
|
@@ -15555,6 +15601,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
15555
15601
|
socket.setBroadcast(true);
|
|
15556
15602
|
} catch {
|
|
15557
15603
|
}
|
|
15604
|
+
this.requestReceiveBuffer(socket);
|
|
15558
15605
|
resolve();
|
|
15559
15606
|
});
|
|
15560
15607
|
});
|
|
@@ -16627,7 +16674,13 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16627
16674
|
clearTimeout(reorder.timer);
|
|
16628
16675
|
reorder.timer = void 0;
|
|
16629
16676
|
}
|
|
16630
|
-
/**
|
|
16677
|
+
/**
|
|
16678
|
+
* Reassemble one in-sequence datagram body into logical frames.
|
|
16679
|
+
*
|
|
16680
|
+
* Frames are packed back to back, so a frame header can itself be cut by a datagram boundary. The start
|
|
16681
|
+
* of a header left at the end of a datagram is carried into the next one rather than discarded; dropping
|
|
16682
|
+
* it would lose that frame and every frame after it until a datagram happened to begin on a header.
|
|
16683
|
+
*/
|
|
16631
16684
|
reassemble(dataType, datagramBody) {
|
|
16632
16685
|
const pending = this.pendingByDataType.get(dataType);
|
|
16633
16686
|
let body = pending ? Buffer.concat([pending.buf, datagramBody]) : datagramBody;
|
|
@@ -16651,6 +16704,9 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16651
16704
|
this.handleFrame(header, payload.subarray(0, header.bytesToRead), dataType);
|
|
16652
16705
|
body = body.subarray(P2P_DATA_HEADER_BYTES + header.bytesToRead);
|
|
16653
16706
|
}
|
|
16707
|
+
if (body.length > 0 && body.length < P2P_DATA_HEADER_BYTES && MAGIC_WORD.startsWith(body.subarray(0, 4).toString())) {
|
|
16708
|
+
this.pendingByDataType.set(dataType, { buf: body });
|
|
16709
|
+
}
|
|
16654
16710
|
}
|
|
16655
16711
|
/**
|
|
16656
16712
|
* Forget where each data type's sequence numbering had reached, and drop any half-reassembled frame.
|
|
@@ -16735,7 +16791,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16735
16791
|
this.emit("error", e instanceof Error ? e : new Error(String(e)));
|
|
16736
16792
|
}
|
|
16737
16793
|
}
|
|
16738
|
-
|
|
16794
|
+
const paddedResult = header.commandId === CMD_SET_PAYLOAD && data.length === PADDED_RESULT_BYTES && data.subarray(4).every((b) => b === 0);
|
|
16795
|
+
if (!isMedia && !frame.json && (data.length === 4 || paddedResult)) {
|
|
16739
16796
|
this.emit("commandResult", { code: data.readInt32LE(0), channel: header.channel });
|
|
16740
16797
|
}
|
|
16741
16798
|
this.emit("data", frame);
|
|
@@ -18164,9 +18221,9 @@ function annexbToJpeg(annexb, spawn2) {
|
|
|
18164
18221
|
|
|
18165
18222
|
// dist/transport/p2p/shared-live-source.js
|
|
18166
18223
|
import { EventEmitter as EventEmitter4 } from "node:events";
|
|
18167
|
-
|
|
18168
|
-
|
|
18169
|
-
return Number.isFinite(
|
|
18224
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
18225
|
+
function timerMs(value, fallback) {
|
|
18226
|
+
return value !== void 0 && Number.isFinite(value) && value >= 1 && value <= MAX_TIMER_DELAY_MS ? value : fallback;
|
|
18170
18227
|
}
|
|
18171
18228
|
function sameConfig(a, b) {
|
|
18172
18229
|
return b !== void 0 && a.codec === b.codec && a.width === b.width && a.height === b.height;
|
|
@@ -18419,14 +18476,14 @@ var SharedLiveSource = class {
|
|
|
18419
18476
|
tag;
|
|
18420
18477
|
constructor(opts) {
|
|
18421
18478
|
this.opts = opts;
|
|
18422
|
-
this.lingerMs = opts.lingerMs
|
|
18479
|
+
this.lingerMs = opts.lingerMs === 0 ? 1 : timerMs(opts.lingerMs, 8e3);
|
|
18423
18480
|
this.maxQueue = opts.maxQueue ?? 900;
|
|
18424
|
-
this.preBufferMs =
|
|
18425
|
-
this.warmRetryMs = opts.warmRetryMs
|
|
18426
|
-
this.warmTimeoutMs = opts.warmTimeoutMs
|
|
18481
|
+
this.preBufferMs = timerMs((opts.preBufferSeconds ?? 0) * 1e3, 0);
|
|
18482
|
+
this.warmRetryMs = timerMs(opts.warmRetryMs, 2e3);
|
|
18483
|
+
this.warmTimeoutMs = timerMs(opts.warmTimeoutMs, 2e4);
|
|
18427
18484
|
this.powered = opts.powered ?? "wired";
|
|
18428
|
-
this.batteryBudgetMs = opts.batteryBudgetMs
|
|
18429
|
-
this.budgetGraceMs = opts.budgetGraceMs
|
|
18485
|
+
this.batteryBudgetMs = timerMs(opts.batteryBudgetMs, 45e3);
|
|
18486
|
+
this.budgetGraceMs = timerMs(opts.budgetGraceMs, 1e4);
|
|
18430
18487
|
this.logger = opts.logger ?? noopLogger;
|
|
18431
18488
|
this.tag = opts.label ? `[live ${opts.label}]` : "[live]";
|
|
18432
18489
|
this.traceId = `pull-${++pullSequence}`;
|
|
@@ -18674,7 +18731,7 @@ var SharedLiveSource = class {
|
|
|
18674
18731
|
if (this.disposed || !this.stream)
|
|
18675
18732
|
return;
|
|
18676
18733
|
this.clearBudget();
|
|
18677
|
-
this.budgetTimer.arm(ms
|
|
18734
|
+
this.budgetTimer.arm(timerMs(ms, this.batteryBudgetMs), () => this.onBudgetExpire());
|
|
18678
18735
|
}
|
|
18679
18736
|
/** Settle a reuse watch on any frame — the join already holds a decodable picture. */
|
|
18680
18737
|
settleReuseWatch() {
|
|
@@ -18815,7 +18872,7 @@ var SharedLiveSource = class {
|
|
|
18815
18872
|
return this.bufferedMedia(seconds).filter((item) => item.kind === "video").map((item) => item.frame);
|
|
18816
18873
|
}
|
|
18817
18874
|
bufferedMedia(seconds) {
|
|
18818
|
-
const requested = Math.min(
|
|
18875
|
+
const requested = Number.isFinite(seconds) ? Math.min(seconds * 1e3, this.preBufferMs) : 0;
|
|
18819
18876
|
if (requested <= 0 || !this.ring.length)
|
|
18820
18877
|
return [];
|
|
18821
18878
|
const start = this.windowStart(Date.now() - requested);
|
|
@@ -26837,7 +26894,7 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26837
26894
|
if (ev.eventName === void 0 && ev.eventType != null)
|
|
26838
26895
|
ev.eventName = detectionName(ev.eventType);
|
|
26839
26896
|
if (ev.thumbnailCandidate)
|
|
26840
|
-
void this.observeStoredImage(ev.thumbnailCandidate);
|
|
26897
|
+
void this.observeStoredImage(ev.thumbnailCandidate, ev.deviceSn);
|
|
26841
26898
|
this.emit("push", ev);
|
|
26842
26899
|
const signal2 = {
|
|
26843
26900
|
source: "push",
|
|
@@ -26886,9 +26943,16 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26886
26943
|
client.on("error", (e) => this.reportError(e));
|
|
26887
26944
|
return client;
|
|
26888
26945
|
}
|
|
26889
|
-
/**
|
|
26890
|
-
|
|
26891
|
-
|
|
26946
|
+
/**
|
|
26947
|
+
* Admit only exact, account-known devices with resolved snapshot evidence into the passive store; a
|
|
26948
|
+
* station-only candidate is admitted under the event's device serial.
|
|
26949
|
+
*/
|
|
26950
|
+
async observeStoredImage(candidate, eventDeviceSn) {
|
|
26951
|
+
if (!this.storedImages)
|
|
26952
|
+
return;
|
|
26953
|
+
const { attribution } = candidate;
|
|
26954
|
+
const deviceSn = attribution.kind === "device" ? attribution.deviceSn : attribution.kind === "station" ? eventDeviceSn : void 0;
|
|
26955
|
+
if (!deviceSn)
|
|
26892
26956
|
return;
|
|
26893
26957
|
const account = this.mega.auth?.userId;
|
|
26894
26958
|
if (!account)
|
|
@@ -26898,9 +26962,9 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26898
26962
|
await this.registry.getDevices();
|
|
26899
26963
|
if (this.mega.auth?.userId !== account)
|
|
26900
26964
|
return;
|
|
26901
|
-
const caps = this.registry.capabilitiesForDevice(
|
|
26965
|
+
const caps = this.registry.capabilitiesForDevice(deviceSn);
|
|
26902
26966
|
if (caps && hasProvidedAction(caps, "snapshotStored")) {
|
|
26903
|
-
this.storedImages.observe(
|
|
26967
|
+
this.storedImages.observe(deviceSn, candidate.url);
|
|
26904
26968
|
}
|
|
26905
26969
|
} catch (e) {
|
|
26906
26970
|
this.reportError(e);
|