@mega-yfue/eufy-sdk 0.5.0-beta.13 → 0.5.0-beta.15
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.
|
@@ -92,6 +92,8 @@ export declare class DeviceRegistry {
|
|
|
92
92
|
private overlayRefusalReported;
|
|
93
93
|
/** Per-serial capability cache for {@link capabilitiesForDevice}; `null` = negative hit. */
|
|
94
94
|
private readonly deviceCapsCache;
|
|
95
|
+
/** Per-serial cloud data points of an AIoT robot, joined beneath the params in {@link record} — see {@link loadCloudDps}. */
|
|
96
|
+
private readonly cloudDps;
|
|
95
97
|
/** Per-serial realtime state, keyed by param id in the device's own namespace (see {@link DeviceRecord.dpParams}). */
|
|
96
98
|
private readonly dpParams;
|
|
97
99
|
/** Callers blocked in {@link awaitRealtimeState}, released by the device's first report. */
|
|
@@ -181,9 +183,26 @@ export declare class DeviceRegistry {
|
|
|
181
183
|
* transport stacks own the claim predicate over the record.
|
|
182
184
|
*/
|
|
183
185
|
require(sn: string): EufyDevice;
|
|
186
|
+
/**
|
|
187
|
+
* Hold an AIoT robot's cloud data points: the cloud's last-known value of each one it has reported,
|
|
188
|
+
* from `getDeviceRelationList`'s per-device `dps`.
|
|
189
|
+
*
|
|
190
|
+
* The device list carries only some of a robot's data points, so a point the robot publishes on change
|
|
191
|
+
* alone (its scene list, its schedules, its consumables) has no value until it next reports. The
|
|
192
|
+
* relation list holds every one, for an account the robot is shared with as well as its owner.
|
|
193
|
+
*
|
|
194
|
+
* Kept apart from the device list's params rather than merged into them: those are what a poll diffs,
|
|
195
|
+
* and a diff retires the realtime report for that id, so a cloud value lagging the robot's last report
|
|
196
|
+
* would replace it. {@link record} joins this beneath both halves. A failure keeps what was held.
|
|
197
|
+
*
|
|
198
|
+
* Limited to {@link CLOUD_DPS_MODELS}, the models whose relation-list `dps` are checked to carry the
|
|
199
|
+
* DP numbers their realtime reports use.
|
|
200
|
+
*/
|
|
201
|
+
private loadCloudDps;
|
|
184
202
|
/**
|
|
185
203
|
* Resolve a serial to a {@link DeviceRecord} with current params: starts from the device-list params, then
|
|
186
|
-
* overlays a fresh `get_device_param_list` when that call is available to this account.
|
|
204
|
+
* overlays a fresh `get_device_param_list` when that call is available to this account. An AIoT robot's
|
|
205
|
+
* cloud data points ({@link loadCloudDps}) sit beneath both, filling only ids neither carries.
|
|
187
206
|
*
|
|
188
207
|
* The overlay is **owner-gated** — a shared or member account is refused it for every device, permanently,
|
|
189
208
|
* which {@link OWNER_ONLY_CODE} identifies — so when it is unavailable the device list is the source
|
package/dist/core/contracts.d.ts
CHANGED
|
@@ -551,16 +551,25 @@ export type AudioCodec = "aac-lc" | "aac-eld" | "g711a";
|
|
|
551
551
|
/**
|
|
552
552
|
* One audio access unit, carrying the codec the station declared for it.
|
|
553
553
|
*
|
|
554
|
-
* Sample rate and channel count are
|
|
555
|
-
* 16 kHz mono for every audio type rather than reading them
|
|
556
|
-
*
|
|
554
|
+
* Sample rate and channel count are absent for `aac-lc` and `g711a`: they are not on the wire, and the v6 app
|
|
555
|
+
* assumes 16 kHz mono for every audio type rather than reading them. An `aac-lc` frame is one ADTS frame and
|
|
556
|
+
* `g711a` is raw A-law. An `aac-eld` frame is one raw access unit with no transport framing, so it carries the
|
|
557
|
+
* AudioSpecificConfig that decodes it.
|
|
557
558
|
*/
|
|
558
|
-
export
|
|
559
|
+
export type LiveAudioFrame = {
|
|
559
560
|
/** Codec declared in the frame header. */
|
|
560
|
-
codec:
|
|
561
|
-
/**
|
|
561
|
+
codec: "aac-lc" | "g711a";
|
|
562
|
+
/** One ADTS frame for `aac-lc`, raw A-law samples for `g711a`. */
|
|
562
563
|
data: Buffer;
|
|
563
|
-
|
|
564
|
+
config?: undefined;
|
|
565
|
+
} | {
|
|
566
|
+
/** Codec declared in the frame header. */
|
|
567
|
+
codec: "aac-eld";
|
|
568
|
+
/** One raw AAC-ELD access unit. */
|
|
569
|
+
data: Buffer;
|
|
570
|
+
/** AudioSpecificConfig (ISO/IEC 14496-3) that decodes `data`. */
|
|
571
|
+
config: Buffer;
|
|
572
|
+
};
|
|
564
573
|
/**
|
|
565
574
|
* One fragmented-MP4 (CMAF) output unit from the native muxer. `init` (the `ftyp`+`moov` init
|
|
566
575
|
* segment) is present exactly once, on the first fragment; every fragment carries a `moof`+`mdat`
|
package/dist/index.js
CHANGED
|
@@ -1996,6 +1996,17 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
1996
1996
|
device_sns: [deviceSn]
|
|
1997
1997
|
});
|
|
1998
1998
|
}
|
|
1999
|
+
/**
|
|
2000
|
+
* Fetch the account's devices as the `devicerelation` service lists them, `{attribute: 7, house_id: ""}`.
|
|
2001
|
+
*
|
|
2002
|
+
* Each entry is `{device: {device_sn, dps, …}}`, and `dps` is the cloud's last-known value of every
|
|
2003
|
+
* data point that device has reported, keyed by DP number. Answered for an account a device is shared
|
|
2004
|
+
* with as well as for its owner. Sent with `category: eufy_home`, without which the service answers
|
|
2005
|
+
* `10000`.
|
|
2006
|
+
*/
|
|
2007
|
+
getDeviceRelationList() {
|
|
2008
|
+
return this.post("devicerelation", "/app/devicerelation/get_device_list", { attribute: 7, house_id: "" }, { category: "eufy_home" });
|
|
2009
|
+
}
|
|
1999
2010
|
/**
|
|
2000
2011
|
* Fetch one page of a device's **cleaning history** from the mega `clean` service.
|
|
2001
2012
|
*
|
|
@@ -17931,6 +17942,7 @@ var AUDIO_HEADER_LEN = 16;
|
|
|
17931
17942
|
var AUDIO_TYPE_OFFSET = 5;
|
|
17932
17943
|
var SC4 = Buffer.from([0, 0, 0, 1]);
|
|
17933
17944
|
var SC3 = Buffer.from([0, 0, 1]);
|
|
17945
|
+
var AAC_ELD_AUDIO_SPECIFIC_CONFIG = Buffer.from("f8f03000", "hex");
|
|
17934
17946
|
var AUDIO_CODECS = { 0: "aac-lc", 2: "g711a", 7: "aac-eld" };
|
|
17935
17947
|
var MAX_TRACED_DECODE_FAILURES = 3;
|
|
17936
17948
|
var DEFAULT_KEEPALIVE_MS = 3e3;
|
|
@@ -18159,7 +18171,7 @@ var LiveStream = class extends EventEmitter3 {
|
|
|
18159
18171
|
if (!codec) {
|
|
18160
18172
|
this.logger.debug(`[live] dropping audio frame: unknown codec id ${codecId ?? "missing"}`);
|
|
18161
18173
|
} else {
|
|
18162
|
-
this.emit("audio", { codec, data: audio });
|
|
18174
|
+
this.emit("audio", codec === "aac-eld" ? { codec, data: audio, config: AAC_ELD_AUDIO_SPECIFIC_CONFIG } : { codec, data: audio });
|
|
18163
18175
|
}
|
|
18164
18176
|
}
|
|
18165
18177
|
}
|
|
@@ -20177,7 +20189,7 @@ var CODEC_BOXES = {
|
|
|
20177
20189
|
h264: { sample: "avc1", config: "avcC" },
|
|
20178
20190
|
h265: { sample: "hvc1", config: "hvcC" }
|
|
20179
20191
|
};
|
|
20180
|
-
var AAC_ELD_SAMPLES_PER_FRAME =
|
|
20192
|
+
var AAC_ELD_SAMPLES_PER_FRAME = 480;
|
|
20181
20193
|
var ADTS_FREQUENCY_INDEX_16K = 8;
|
|
20182
20194
|
var ADTS_CHANNELS_MONO = 1;
|
|
20183
20195
|
var Fmp4Muxer = class {
|
|
@@ -20195,6 +20207,8 @@ var Fmp4Muxer = class {
|
|
|
20195
20207
|
lastVideoTimestampMs;
|
|
20196
20208
|
audioRequested;
|
|
20197
20209
|
audioCodec;
|
|
20210
|
+
/** Decoder config of an AAC-ELD track, taken from its frames; absent for AAC-LC, whose `esds` config is fixed. */
|
|
20211
|
+
audioConfig;
|
|
20198
20212
|
audioDisabled = false;
|
|
20199
20213
|
audioSamples = [];
|
|
20200
20214
|
audioBaseDecodeTime = 0;
|
|
@@ -20261,7 +20275,10 @@ var Fmp4Muxer = class {
|
|
|
20261
20275
|
}
|
|
20262
20276
|
return void 0;
|
|
20263
20277
|
}
|
|
20264
|
-
/**
|
|
20278
|
+
/**
|
|
20279
|
+
* Add one station-declared AAC access unit to the audio track: an ADTS frame for AAC-LC, a raw access unit
|
|
20280
|
+
* described by its frame's config for AAC-ELD. G.711 remains available via `live()`.
|
|
20281
|
+
*/
|
|
20265
20282
|
pushAudio(frame, timestampMs = now()) {
|
|
20266
20283
|
if (!this.audioRequested || this.audioDisabled)
|
|
20267
20284
|
return void 0;
|
|
@@ -20277,12 +20294,19 @@ var Fmp4Muxer = class {
|
|
|
20277
20294
|
if (this.audioCodec && this.audioCodec !== frame.codec) {
|
|
20278
20295
|
return this.disableAudio();
|
|
20279
20296
|
}
|
|
20280
|
-
|
|
20281
|
-
if (
|
|
20282
|
-
|
|
20283
|
-
|
|
20284
|
-
|
|
20285
|
-
|
|
20297
|
+
let sample;
|
|
20298
|
+
if (frame.codec === "aac-eld") {
|
|
20299
|
+
this.audioConfig = frame.config;
|
|
20300
|
+
sample = frame.data;
|
|
20301
|
+
} else {
|
|
20302
|
+
const header = parseAdtsHeader(frame.data);
|
|
20303
|
+
if (!header || header.frameLength > frame.data.length) {
|
|
20304
|
+
throw new Error(`fMP4 muxer: ${frame.codec} frame is not a complete ADTS access unit`);
|
|
20305
|
+
}
|
|
20306
|
+
if (header.frequencyIndex !== ADTS_FREQUENCY_INDEX_16K || header.channels !== ADTS_CHANNELS_MONO) {
|
|
20307
|
+
return this.disableAudio();
|
|
20308
|
+
}
|
|
20309
|
+
sample = frame.data.subarray(header.headerLength, header.frameLength);
|
|
20286
20310
|
}
|
|
20287
20311
|
this.audioCodec = frame.codec;
|
|
20288
20312
|
this.firstAudioTimestampMs ??= timestampMs;
|
|
@@ -20294,7 +20318,7 @@ var Fmp4Muxer = class {
|
|
|
20294
20318
|
this.audioSamples[this.audioSamples.length - 1].duration = measured;
|
|
20295
20319
|
}
|
|
20296
20320
|
this.audioSamples.push({
|
|
20297
|
-
data:
|
|
20321
|
+
data: sample,
|
|
20298
20322
|
duration: frame.codec === "aac-eld" ? AAC_ELD_SAMPLES_PER_FRAME : AAC_SAMPLES_PER_FRAME,
|
|
20299
20323
|
keyframe: true
|
|
20300
20324
|
});
|
|
@@ -20567,7 +20591,7 @@ var Fmp4Muxer = class {
|
|
|
20567
20591
|
return box("mp4a", head, this.esds());
|
|
20568
20592
|
}
|
|
20569
20593
|
esds() {
|
|
20570
|
-
const config = this.
|
|
20594
|
+
const config = this.audioConfig ?? Buffer.from([20, 8]);
|
|
20571
20595
|
const specific = descriptor(5, config);
|
|
20572
20596
|
const decoder = descriptor(4, Buffer.concat([Buffer.from([64, 21, 0, 0, 0]), u32(64e3), u32(32e3), specific]));
|
|
20573
20597
|
const sl = descriptor(6, Buffer.from([2]));
|
|
@@ -26318,6 +26342,7 @@ function deviceClassOf(codec, realtime) {
|
|
|
26318
26342
|
return cls === "camera" && realtime !== "p2p" ? "other" : cls;
|
|
26319
26343
|
}
|
|
26320
26344
|
var LIST_REUSE_MS = 5e3;
|
|
26345
|
+
var CLOUD_DPS_MODELS = /* @__PURE__ */ new Set(["T2351"]);
|
|
26321
26346
|
var DeviceRegistry = class {
|
|
26322
26347
|
mega;
|
|
26323
26348
|
onError;
|
|
@@ -26342,6 +26367,8 @@ var DeviceRegistry = class {
|
|
|
26342
26367
|
overlayRefusalReported = false;
|
|
26343
26368
|
/** Per-serial capability cache for {@link capabilitiesForDevice}; `null` = negative hit. */
|
|
26344
26369
|
deviceCapsCache = /* @__PURE__ */ new Map();
|
|
26370
|
+
/** Per-serial cloud data points of an AIoT robot, joined beneath the params in {@link record} — see {@link loadCloudDps}. */
|
|
26371
|
+
cloudDps = /* @__PURE__ */ new Map();
|
|
26345
26372
|
/** Per-serial realtime state, keyed by param id in the device's own namespace (see {@link DeviceRecord.dpParams}). */
|
|
26346
26373
|
dpParams = /* @__PURE__ */ new Map();
|
|
26347
26374
|
/** Callers blocked in {@link awaitRealtimeState}, released by the device's first report. */
|
|
@@ -26451,6 +26478,7 @@ var DeviceRegistry = class {
|
|
|
26451
26478
|
});
|
|
26452
26479
|
}
|
|
26453
26480
|
}
|
|
26481
|
+
await this.loadCloudDps(seen);
|
|
26454
26482
|
this.lastRefreshPartial = partial;
|
|
26455
26483
|
if (partial) {
|
|
26456
26484
|
for (const prev of this.devices)
|
|
@@ -26541,9 +26569,46 @@ var DeviceRegistry = class {
|
|
|
26541
26569
|
}
|
|
26542
26570
|
return dev;
|
|
26543
26571
|
}
|
|
26572
|
+
/**
|
|
26573
|
+
* Hold an AIoT robot's cloud data points: the cloud's last-known value of each one it has reported,
|
|
26574
|
+
* from `getDeviceRelationList`'s per-device `dps`.
|
|
26575
|
+
*
|
|
26576
|
+
* The device list carries only some of a robot's data points, so a point the robot publishes on change
|
|
26577
|
+
* alone (its scene list, its schedules, its consumables) has no value until it next reports. The
|
|
26578
|
+
* relation list holds every one, for an account the robot is shared with as well as its owner.
|
|
26579
|
+
*
|
|
26580
|
+
* Kept apart from the device list's params rather than merged into them: those are what a poll diffs,
|
|
26581
|
+
* and a diff retires the realtime report for that id, so a cloud value lagging the robot's last report
|
|
26582
|
+
* would replace it. {@link record} joins this beneath both halves. A failure keeps what was held.
|
|
26583
|
+
*
|
|
26584
|
+
* Limited to {@link CLOUD_DPS_MODELS}, the models whose relation-list `dps` are checked to carry the
|
|
26585
|
+
* DP numbers their realtime reports use.
|
|
26586
|
+
*/
|
|
26587
|
+
async loadCloudDps(seen) {
|
|
26588
|
+
const robots = [...seen.values()].filter((d) => CLOUD_DPS_MODELS.has(d.model ?? ""));
|
|
26589
|
+
if (!robots.length)
|
|
26590
|
+
return;
|
|
26591
|
+
let res;
|
|
26592
|
+
try {
|
|
26593
|
+
res = await this.mega.getDeviceRelationList();
|
|
26594
|
+
} catch (e) {
|
|
26595
|
+
if (e instanceof SessionExpiredError)
|
|
26596
|
+
throw e;
|
|
26597
|
+
this.logger.debug(`[registry] device relation list unavailable: ${e?.message ?? e}`);
|
|
26598
|
+
return;
|
|
26599
|
+
}
|
|
26600
|
+
const wanted = new Set(robots.map((d) => d.sn));
|
|
26601
|
+
for (const entry of res.devices ?? []) {
|
|
26602
|
+
const sn = entry?.device?.device_sn;
|
|
26603
|
+
const dps = parseAiotDpReport({ payload: entry?.device?.dps });
|
|
26604
|
+
if (sn && wanted.has(sn) && dps)
|
|
26605
|
+
this.cloudDps.set(sn, dps);
|
|
26606
|
+
}
|
|
26607
|
+
}
|
|
26544
26608
|
/**
|
|
26545
26609
|
* Resolve a serial to a {@link DeviceRecord} with current params: starts from the device-list params, then
|
|
26546
|
-
* overlays a fresh `get_device_param_list` when that call is available to this account.
|
|
26610
|
+
* overlays a fresh `get_device_param_list` when that call is available to this account. An AIoT robot's
|
|
26611
|
+
* cloud data points ({@link loadCloudDps}) sit beneath both, filling only ids neither carries.
|
|
26547
26612
|
*
|
|
26548
26613
|
* The overlay is **owner-gated** — a shared or member account is refused it for every device, permanently,
|
|
26549
26614
|
* which {@link OWNER_ONLY_CODE} identifies — so when it is unavailable the device list is the source
|
|
@@ -26572,7 +26637,7 @@ var DeviceRegistry = class {
|
|
|
26572
26637
|
let dev = this.devices.find((d) => d.sn === sn);
|
|
26573
26638
|
if (!dev)
|
|
26574
26639
|
throw new Error(`device ${sn} not found (have: ${this.devices.map((d) => d.sn).join(", ")})`);
|
|
26575
|
-
const params = { ...dev.params ?? {} };
|
|
26640
|
+
const params = { ...this.cloudDps.get(sn), ...dev.params ?? {} };
|
|
26576
26641
|
const paramUpdatedAt = { ...dev.paramUpdatedAt ?? {} };
|
|
26577
26642
|
if (!this.overlayRefused.has(sn)) {
|
|
26578
26643
|
try {
|