@mega-yfue/eufy-sdk 0.4.0-beta.16 → 0.4.0-beta.17
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 +18 -2
- package/dist/client/types.d.ts +25 -0
- package/dist/index.js +129 -7
- package/dist/index.js.map +3 -3
- package/dist/transport/p2p/p2p-session.d.ts +23 -0
- package/package.json +1 -1
|
@@ -20,8 +20,8 @@ import { type P2PSession } from "../transport/p2p/p2p-session.js";
|
|
|
20
20
|
import { type CleanRecordPage } from "../model/clean-records.js";
|
|
21
21
|
import { type AvailabilityObservation, type EufyDevice } from "../core/types.js";
|
|
22
22
|
import { Device, type DeviceInspection } from "../model/index.js";
|
|
23
|
-
import type { EufyMegaOptions, EufyMegaEvent, EufyMegaEventMap, DeviceState, RealtimeReadiness, WaitForRealtimeOptions } from "./types.js";
|
|
24
|
-
export type { EufyMegaOptions, EufyMegaEvent, EufyMegaEventMap, AnyDeviceEvent, DeviceState, RealtimePlaneReadiness, RealtimeReadiness, WaitForRealtimeOptions, } from "./types.js";
|
|
23
|
+
import type { EufyMegaOptions, EufyMegaEvent, EufyMegaEventMap, DeviceState, RealtimeReadiness, StationFace, StationFacesOptions, WaitForRealtimeOptions } from "./types.js";
|
|
24
|
+
export type { EufyMegaOptions, EufyMegaEvent, EufyMegaEventMap, AnyDeviceEvent, DeviceState, RealtimePlaneReadiness, RealtimeReadiness, StationFace, StationFacesOptions, WaitForRealtimeOptions, } from "./types.js";
|
|
25
25
|
export interface EufyMega {
|
|
26
26
|
on<E extends EufyMegaEvent>(event: E, listener: (...args: EufyMegaEventMap[E]) => void): this;
|
|
27
27
|
once<E extends EufyMegaEvent>(event: E, listener: (...args: EufyMegaEventMap[E]) => void): this;
|
|
@@ -764,6 +764,22 @@ export declare class EufyMega extends EventEmitter {
|
|
|
764
764
|
* every event the station's own session already delivers.
|
|
765
765
|
*/
|
|
766
766
|
getP2pSessions(): Map<string, P2PSession>;
|
|
767
|
+
/**
|
|
768
|
+
* The people enrolled on a station, read from its own `person_basic_info` table over P2P.
|
|
769
|
+
*
|
|
770
|
+
* The cloud roster (`api.getFaces()`) answers empty for an account that keeps its faces on the
|
|
771
|
+
* station, because they were never uploaded — the station holds them.
|
|
772
|
+
*
|
|
773
|
+
* Resolves with the rows the station returned. Rejects on a station that cannot be reached, on a
|
|
774
|
+
* record stating no `admin_user_id`, and on the reply not arriving whole in time. Assembling that
|
|
775
|
+
* reply is `P2PSession.readDatabase`'s; this resolves which station and which account id.
|
|
776
|
+
*
|
|
777
|
+
* `stranger<n>` names are the station's own placeholders for a face nobody has named, and are
|
|
778
|
+
* returned as they arrive; a caller that wants only named people filters them.
|
|
779
|
+
*/
|
|
780
|
+
getStationFaces(stationSn: string, opts?: StationFacesOptions): Promise<StationFace[]>;
|
|
781
|
+
/** The station's own `admin_user_id`, which its database refuses the query without. */
|
|
782
|
+
private adminUserIdOf;
|
|
767
783
|
/**
|
|
768
784
|
* The liveness facts for one device — see {@link DeviceState}. Facts, not an `online` verdict: "how
|
|
769
785
|
* long is too long" is a threshold that belongs to the caller, and it differs per device class.
|
package/dist/client/types.d.ts
CHANGED
|
@@ -221,6 +221,31 @@ export interface DeviceState {
|
|
|
221
221
|
*/
|
|
222
222
|
lastSeenMs?: number;
|
|
223
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* One person enrolled on a station, as its `person_basic_info` table states them.
|
|
226
|
+
*
|
|
227
|
+
* The station's own table, not the cloud's: an account that keeps its faces on the station never
|
|
228
|
+
* uploaded them, so the cloud roster answers empty for it while this answers the household.
|
|
229
|
+
*
|
|
230
|
+
* `person_id` is the id an `IDENTITY_PERSON_DETECTION` push carries, which is what makes a detection
|
|
231
|
+
* nameable. Every field is optional because the row is the station's to shape — an absent one is a
|
|
232
|
+
* column that row did not carry, never a default — and unrecognised columns are kept verbatim, so a
|
|
233
|
+
* station that states more than this is not truncated by it. Keys are the table's own, uncased.
|
|
234
|
+
*/
|
|
235
|
+
export interface StationFace {
|
|
236
|
+
person_id?: number;
|
|
237
|
+
/** The name this person was enrolled under. `stranger<n>` is the station's own placeholder. */
|
|
238
|
+
name?: string;
|
|
239
|
+
relation?: number | string;
|
|
240
|
+
group_id?: number | string;
|
|
241
|
+
[column: string]: unknown;
|
|
242
|
+
}
|
|
243
|
+
/** How long to wait for a station's database reply, and how to give up early. */
|
|
244
|
+
export interface StationFacesOptions {
|
|
245
|
+
/** Default 15s. A full table is a handful of frames; a station that will not answer says nothing. */
|
|
246
|
+
timeoutMs?: number;
|
|
247
|
+
signal?: AbortSignal;
|
|
248
|
+
}
|
|
224
249
|
/**
|
|
225
250
|
* A single semantic event tagged with its name — the payload of the catch-all `"event"` listener.
|
|
226
251
|
* A discriminated union over {@link DeviceEventMap}, so switching on `e.eventName` narrows `e` to that
|
package/dist/index.js
CHANGED
|
@@ -15266,6 +15266,49 @@ function paramReport(payload) {
|
|
|
15266
15266
|
return Object.keys(out).length ? out : void 0;
|
|
15267
15267
|
}
|
|
15268
15268
|
var traceSequence = 0;
|
|
15269
|
+
var DB_TABLE_TIMEOUT_MS = 15e3;
|
|
15270
|
+
function firstTableRows(text2) {
|
|
15271
|
+
const decoded = Buffer.from(text2, "latin1").toString("utf8");
|
|
15272
|
+
for (let start = decoded.indexOf("{"); start >= 0; start = decoded.indexOf("{", start + 1)) {
|
|
15273
|
+
const end = closingBrace(decoded, start);
|
|
15274
|
+
if (end < 0)
|
|
15275
|
+
return void 0;
|
|
15276
|
+
let parsed;
|
|
15277
|
+
try {
|
|
15278
|
+
parsed = JSON.parse(decoded.slice(start, end + 1));
|
|
15279
|
+
} catch {
|
|
15280
|
+
continue;
|
|
15281
|
+
}
|
|
15282
|
+
const data = parsed?.data;
|
|
15283
|
+
if (Array.isArray(data))
|
|
15284
|
+
return data;
|
|
15285
|
+
}
|
|
15286
|
+
return void 0;
|
|
15287
|
+
}
|
|
15288
|
+
function closingBrace(text2, start) {
|
|
15289
|
+
let depth = 0;
|
|
15290
|
+
let inString = false;
|
|
15291
|
+
let escaped = false;
|
|
15292
|
+
for (let i = start; i < text2.length; i++) {
|
|
15293
|
+
const c = text2[i];
|
|
15294
|
+
if (inString) {
|
|
15295
|
+
if (escaped)
|
|
15296
|
+
escaped = false;
|
|
15297
|
+
else if (c === "\\")
|
|
15298
|
+
escaped = true;
|
|
15299
|
+
else if (c === '"')
|
|
15300
|
+
inString = false;
|
|
15301
|
+
continue;
|
|
15302
|
+
}
|
|
15303
|
+
if (c === '"')
|
|
15304
|
+
inString = true;
|
|
15305
|
+
else if (c === "{")
|
|
15306
|
+
depth++;
|
|
15307
|
+
else if (c === "}" && --depth === 0)
|
|
15308
|
+
return i;
|
|
15309
|
+
}
|
|
15310
|
+
return -1;
|
|
15311
|
+
}
|
|
15269
15312
|
var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
15270
15313
|
cfg;
|
|
15271
15314
|
socket;
|
|
@@ -16495,8 +16538,14 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16495
16538
|
* The HomeBase streams back `CMD_DATABASE` (1306) frames `{cmd:10000,count,data:[…]}`,
|
|
16496
16539
|
* level-1-encrypted — decoded and emitted as `dbChunk` (decrypted text) per frame.
|
|
16497
16540
|
* Tables: `familiar_faces`, `person_basic_info`, `event_person_list`, `history_record_info`.
|
|
16541
|
+
*
|
|
16542
|
+
* Throws while a {@link readDatabase} is accumulating: every table answers `{data:[…]}` and the
|
|
16543
|
+
* frames tie no chunk to its request, so a second query's reply would be assembled into the first
|
|
16544
|
+
* one's buffer and answered as its rows.
|
|
16498
16545
|
*/
|
|
16499
16546
|
queryDatabase(table, opts = {}) {
|
|
16547
|
+
if (this.dbReadInFlight)
|
|
16548
|
+
throw new Error(`queryDatabase: ${this.cfg.stationSn} is already reading a table`);
|
|
16500
16549
|
if (!this.connectAddress)
|
|
16501
16550
|
throw new Error("not connected");
|
|
16502
16551
|
const channel = opts.channel ?? STATION_CHANNEL3;
|
|
@@ -16559,6 +16608,49 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16559
16608
|
query: this.fullTableQuery()
|
|
16560
16609
|
});
|
|
16561
16610
|
}
|
|
16611
|
+
/**
|
|
16612
|
+
* Query one on-station table and answer its rows, once the reply is whole.
|
|
16613
|
+
*
|
|
16614
|
+
* The request half of {@link queryDatabase} with its reply assembled: `CMD_DATABASE` arrives as
|
|
16615
|
+
* several frames whose decrypted text is a fragment of one document, so the fragments are
|
|
16616
|
+
* accumulated here and scanned after each one. Answers that document's `data` rows. Rejects when
|
|
16617
|
+
* `signal` aborts, and when `timeoutMs` (default {@link DB_TABLE_TIMEOUT_MS}) elapses with no
|
|
16618
|
+
* complete reply.
|
|
16619
|
+
*
|
|
16620
|
+
* One read at a time per session: while one is accumulating, every {@link queryDatabase} on the
|
|
16621
|
+
* session throws, so a second reply cannot land in this buffer.
|
|
16622
|
+
*/
|
|
16623
|
+
async readDatabase(table, opts = {}) {
|
|
16624
|
+
if (opts.signal?.aborted)
|
|
16625
|
+
throw new Error("readDatabase: aborted");
|
|
16626
|
+
this.queryDatabase(table, { accountId: opts.accountId, query: this.fullTableQuery() });
|
|
16627
|
+
this.dbReadInFlight = true;
|
|
16628
|
+
try {
|
|
16629
|
+
return await new Promise((resolve, reject) => {
|
|
16630
|
+
let text2 = "";
|
|
16631
|
+
const onChunk = (chunk) => {
|
|
16632
|
+
text2 += chunk.text;
|
|
16633
|
+
const rows = firstTableRows(text2);
|
|
16634
|
+
if (rows)
|
|
16635
|
+
settle(() => resolve(rows));
|
|
16636
|
+
};
|
|
16637
|
+
const timer = setTimeout(() => settle(() => reject(new Error(`readDatabase: ${this.cfg.stationSn} sent no complete ${table}`))), opts.timeoutMs ?? DB_TABLE_TIMEOUT_MS);
|
|
16638
|
+
const onAbort = () => settle(() => reject(new Error("readDatabase: aborted")));
|
|
16639
|
+
const settle = (finish) => {
|
|
16640
|
+
clearTimeout(timer);
|
|
16641
|
+
this.off("dbChunk", onChunk);
|
|
16642
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
16643
|
+
finish();
|
|
16644
|
+
};
|
|
16645
|
+
this.on("dbChunk", onChunk);
|
|
16646
|
+
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
16647
|
+
});
|
|
16648
|
+
} finally {
|
|
16649
|
+
this.dbReadInFlight = false;
|
|
16650
|
+
}
|
|
16651
|
+
}
|
|
16652
|
+
/** Whether a {@link readDatabase} is accumulating; see {@link queryDatabase} for what it bars. */
|
|
16653
|
+
dbReadInFlight = false;
|
|
16562
16654
|
/**
|
|
16563
16655
|
* Request the **face feature rows** over P2P (`face_feature_info`, inner `cmd 10000`). Each row
|
|
16564
16656
|
* carries `{person_id, face_name, face_id, face_picture_content, face_feature_file_path}` — where
|
|
@@ -25380,6 +25472,11 @@ function recordString(raw, key) {
|
|
|
25380
25472
|
const v = typeof nested === "string" && nested ? nested : raw[key];
|
|
25381
25473
|
return typeof v === "string" && v ? v : void 0;
|
|
25382
25474
|
}
|
|
25475
|
+
function adminUserIdFrom(raw) {
|
|
25476
|
+
const member = (raw ?? {}).member;
|
|
25477
|
+
const id = member?.admin_user_id;
|
|
25478
|
+
return typeof id === "string" && id ? id : void 0;
|
|
25479
|
+
}
|
|
25383
25480
|
function tuyaDevIdFrom(raw) {
|
|
25384
25481
|
for (const field of ["tuya_uuid", "tuya_virtual_id", "tuya_device_id", "virtualId"]) {
|
|
25385
25482
|
const v = recordString(raw, field);
|
|
@@ -25537,13 +25634,7 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
25537
25634
|
onError: (e) => this.reportError(e),
|
|
25538
25635
|
// The `a2` account id an `eufy_life` DP frame embeds — the owning member's `admin_user_id`,
|
|
25539
25636
|
// falling back to the logged-in account's user id (the confirmed script path does the same).
|
|
25540
|
-
resolveAccountId: (dev) =>
|
|
25541
|
-
const member = (dev.raw ?? {}).member;
|
|
25542
|
-
const adminId = member?.admin_user_id;
|
|
25543
|
-
if (typeof adminId === "string" && adminId)
|
|
25544
|
-
return adminId;
|
|
25545
|
-
return this.mega.auth?.userId ?? "";
|
|
25546
|
-
},
|
|
25637
|
+
resolveAccountId: (dev) => adminUserIdFrom(dev.raw) ?? this.mega.auth?.userId ?? "",
|
|
25547
25638
|
// Fetch + parse a gallery effect from the HTTP catalogue into the serializable spec. The catalogue
|
|
25548
25639
|
// lives in the http layer (transport/http/light-catalog); the mqtt router owns the frame bytes.
|
|
25549
25640
|
resolvePreset: (presetId) => resolveLightEffect(this.mega, presetId),
|
|
@@ -26990,6 +27081,37 @@ var EufyMega = class extends EventEmitter9 {
|
|
|
26990
27081
|
getP2pSessions() {
|
|
26991
27082
|
return this.p2p.getSessions();
|
|
26992
27083
|
}
|
|
27084
|
+
/**
|
|
27085
|
+
* The people enrolled on a station, read from its own `person_basic_info` table over P2P.
|
|
27086
|
+
*
|
|
27087
|
+
* The cloud roster (`api.getFaces()`) answers empty for an account that keeps its faces on the
|
|
27088
|
+
* station, because they were never uploaded — the station holds them.
|
|
27089
|
+
*
|
|
27090
|
+
* Resolves with the rows the station returned. Rejects on a station that cannot be reached, on a
|
|
27091
|
+
* record stating no `admin_user_id`, and on the reply not arriving whole in time. Assembling that
|
|
27092
|
+
* reply is `P2PSession.readDatabase`'s; this resolves which station and which account id.
|
|
27093
|
+
*
|
|
27094
|
+
* `stranger<n>` names are the station's own placeholders for a face nobody has named, and are
|
|
27095
|
+
* returned as they arrive; a caller that wants only named people filters them.
|
|
27096
|
+
*/
|
|
27097
|
+
async getStationFaces(stationSn, opts = {}) {
|
|
27098
|
+
if (!this.registry.list().length)
|
|
27099
|
+
await this.getDevices();
|
|
27100
|
+
const station = this.p2p.stationKeyOf(stationSn);
|
|
27101
|
+
const accountId = this.adminUserIdOf(station);
|
|
27102
|
+
if (!accountId)
|
|
27103
|
+
throw new Error(`getStationFaces: no admin_user_id on record for ${station}`);
|
|
27104
|
+
await this.p2p.ensureStation(station, opts.signal);
|
|
27105
|
+
const session = this.p2p.getSessions().get(station);
|
|
27106
|
+
if (!session)
|
|
27107
|
+
throw new Error(`getStationFaces: no P2P session for ${station}`);
|
|
27108
|
+
const rows = await session.readDatabase("person_basic_info", { accountId, ...opts });
|
|
27109
|
+
return rows.filter((row) => typeof row === "object" && row !== null);
|
|
27110
|
+
}
|
|
27111
|
+
/** The station's own `admin_user_id`, which its database refuses the query without. */
|
|
27112
|
+
adminUserIdOf(stationSn) {
|
|
27113
|
+
return adminUserIdFrom(this.registry.list().find((d) => d.sn === stationSn)?.raw);
|
|
27114
|
+
}
|
|
26993
27115
|
/**
|
|
26994
27116
|
* The liveness facts for one device — see {@link DeviceState}. Facts, not an `online` verdict: "how
|
|
26995
27117
|
* long is too long" is a threshold that belongs to the caller, and it differs per device class.
|