@mega-yfue/eufy-sdk 0.6.0-beta.17 → 0.6.0-beta.19
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/device-registry.d.ts +6 -0
- package/dist/client/eufy-mega.d.ts +20 -2
- package/dist/client/types.d.ts +9 -6
- package/dist/core/types.d.ts +4 -2
- package/dist/index.js +141 -34
- package/dist/index.js.map +3 -3
- package/dist/model/capabilities/vacuum-clean.d.ts +23 -0
- package/dist/transport/mqtt/biz-stream.d.ts +22 -25
- package/dist/transport/p2p/command-router.d.ts +2 -0
- package/dist/transport/p2p/live-stream.d.ts +2 -1
- package/dist/transport/p2p/p2p-session.d.ts +12 -1
- package/package.json +1 -1
|
@@ -248,6 +248,12 @@ export declare class DeviceRegistry {
|
|
|
248
248
|
* for an expired token is the same lie {@link getDevices} stopped telling, one level down.
|
|
249
249
|
*/
|
|
250
250
|
refreshedList(sn: string): Promise<EufyDevice | undefined>;
|
|
251
|
+
/**
|
|
252
|
+
* Record that `sn` was heard on a realtime wire at `atMs`, advancing its {@link EufyDevice.lastSeenMs}.
|
|
253
|
+
*
|
|
254
|
+
* Only ever moves the time forward, and survives a {@link getDevices} rebuild.
|
|
255
|
+
*/
|
|
256
|
+
markSeen(sn: string, atMs: number): void;
|
|
251
257
|
/**
|
|
252
258
|
* Record state a device reported over its realtime wire, merging into whatever it last reported.
|
|
253
259
|
*
|
|
@@ -129,6 +129,12 @@ export declare class EufyMega extends EventEmitter {
|
|
|
129
129
|
private readonly pollTimer;
|
|
130
130
|
/** Armed while a failed push bring-up waits to be retried; cancelled by {@link disconnect}. */
|
|
131
131
|
private readonly pushRetryTimer;
|
|
132
|
+
/**
|
|
133
|
+
* Per wired station, the pending reopen of its session and the pause before the next one; see
|
|
134
|
+
* {@link reopenWiredStation}. Dropped for a station when its session connects, and wholly by
|
|
135
|
+
* {@link disconnect}.
|
|
136
|
+
*/
|
|
137
|
+
private readonly stationReopens;
|
|
132
138
|
constructor(opts: EufyMegaOptions);
|
|
133
139
|
/**
|
|
134
140
|
* Builds the inbound listener that converts ThingClips DP maps to realtime capability state.
|
|
@@ -313,7 +319,16 @@ export declare class EufyMega extends EventEmitter {
|
|
|
313
319
|
* manager's own log, since by then nothing is waiting to receive it.
|
|
314
320
|
*/
|
|
315
321
|
private recycleStandaloneSession;
|
|
316
|
-
/**
|
|
322
|
+
/**
|
|
323
|
+
* Reopen a wired station's session while the generation that warmed it is current: after
|
|
324
|
+
* {@link recycleStandaloneSession} closed it, after it closed on its own, or after its warm-up failed.
|
|
325
|
+
*
|
|
326
|
+
* A wired station's session is the persistent one realtime start opened, so a station left without one
|
|
327
|
+
* receives nothing until a command dials it again. The first reopen runs at once; one that fails is
|
|
328
|
+
* retried after {@link STATION_REOPEN_MIN_MS}, doubling up to {@link STATION_REOPEN_MAX_MS}, until the
|
|
329
|
+
* session connects or the generation ends. One reopen is pending per station at a time, and each failure
|
|
330
|
+
* is reported once, by {@link P2PCommandRouter.ensureStation}.
|
|
331
|
+
*/
|
|
317
332
|
private reopenWiredStation;
|
|
318
333
|
/**
|
|
319
334
|
* Whether the DECODED property now reads what the write asked for.
|
|
@@ -339,6 +354,8 @@ export declare class EufyMega extends EventEmitter {
|
|
|
339
354
|
* fans same-kind sensors out by channel, so the resolved serial is folded in; a payload's own
|
|
340
355
|
* fields still win on conflict.
|
|
341
356
|
*
|
|
357
|
+
* A station's camera-info table lands per channel, each channel's params on the device attached there.
|
|
358
|
+
*
|
|
342
359
|
* Params the station volunteered land as device state first. They arrive in the cloud record's own
|
|
343
360
|
* `param_type → value` shape, so no capability has to claim an id it does not own, and the reads
|
|
344
361
|
* they back stop waiting for the next cloud poll. A capability decode follows for the frames that
|
|
@@ -754,7 +771,8 @@ export declare class EufyMega extends EventEmitter {
|
|
|
754
771
|
*/
|
|
755
772
|
keepStationWarm(stationSn: string): () => void;
|
|
756
773
|
/** Eagerly open P2P sessions for WIRED stations only (persistent — they don't drain). Battery
|
|
757
|
-
* stations stay closed until an on-demand open. Best-effort per station
|
|
774
|
+
* stations stay closed until an on-demand open. Best-effort per station: one that fails is handed to
|
|
775
|
+
* {@link reopenWiredStation}. */
|
|
758
776
|
private warmWiredP2P;
|
|
759
777
|
/**
|
|
760
778
|
* A station's power tier for the P2P lifecycle: a HomeBase/station is `"wired"` (persistent); a
|
package/dist/client/types.d.ts
CHANGED
|
@@ -202,9 +202,11 @@ export interface EufyMegaOptions extends MegaClientConfig {
|
|
|
202
202
|
* that choice on every host. The SDK reports when the device last spoke; the caller decides what that
|
|
203
203
|
* means — the same split as the snapshot cache TTL and the live power budget.
|
|
204
204
|
*
|
|
205
|
-
* P2P session
|
|
206
|
-
* when something needs one and closed when idle, so "no session" is the resting state of a healthy
|
|
207
|
-
* device.
|
|
205
|
+
* The absence of a P2P session is **not** a liveness signal and is not carried here: sessions are opened
|
|
206
|
+
* only when something needs one and closed when idle, so "no session" is the resting state of a healthy
|
|
207
|
+
* device. A station answering an open session is the opposite fact, positive evidence, and does advance
|
|
208
|
+
* the station's own {@link DeviceState.lastSeenMs} — never that of a device it covers, whose liveness its answer does
|
|
209
|
+
* not show. Transport visibility lives on `getP2pSessions()` and the `p2pConnect`/`p2pClose` events,
|
|
208
210
|
* station-scoped like the session itself.
|
|
209
211
|
*
|
|
210
212
|
* The cloud record carries no connectivity field either: it has no `status` / `device_online`, and the
|
|
@@ -215,8 +217,9 @@ export interface DeviceState {
|
|
|
215
217
|
/** The parent station whose P2P session covers this device (itself, when standalone). */
|
|
216
218
|
stationSn: string;
|
|
217
219
|
/**
|
|
218
|
-
* When the device last
|
|
219
|
-
*
|
|
220
|
+
* When the device was last heard from, in ms: its last report to the cloud, or a later one over a
|
|
221
|
+
* realtime wire, a station's heartbeat answer on its open P2P session included. The cloud half is bounded
|
|
222
|
+
* by the cloud's own slow refresh — minutes, not seconds — so it answers "is this device alive at all",
|
|
220
223
|
* not "what is it doing right now".
|
|
221
224
|
*/
|
|
222
225
|
lastSeenMs?: number;
|
|
@@ -332,7 +335,7 @@ export type EufyMegaEventMap = {
|
|
|
332
335
|
map: VacuumMapSnapshot;
|
|
333
336
|
}];
|
|
334
337
|
/**
|
|
335
|
-
* A device
|
|
338
|
+
* A device was heard from since the last poll — its {@link DeviceState.lastSeenMs} advanced.
|
|
336
339
|
* Carries {@link DeviceState}; the host applies its own staleness threshold.
|
|
337
340
|
*
|
|
338
341
|
* Transport/session lifecycle is NOT this event: that's `p2pConnect`/`p2pClose`, station-scoped where
|
package/dist/core/types.d.ts
CHANGED
|
@@ -80,8 +80,10 @@ export interface EufyDevice {
|
|
|
80
80
|
*/
|
|
81
81
|
paramUpdatedAt?: Record<number, number>;
|
|
82
82
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
83
|
+
* When the device was last heard from, in **milliseconds** (comparable to `Date.now()` directly): the
|
|
84
|
+
* most recent {@link paramUpdatedAt} across all of its params, or a later report over a realtime wire,
|
|
85
|
+
* including a station answering its own P2P session's heartbeat. Never moves backwards. `undefined` when
|
|
86
|
+
* neither has been seen.
|
|
85
87
|
*
|
|
86
88
|
* This is an observation — "the device last reported at T" — deliberately **not** a reachability
|
|
87
89
|
* verdict. How long a silence means "unreachable" depends on the device (a mains camera heartbeats
|
package/dist/index.js
CHANGED
|
@@ -2981,16 +2981,13 @@ var BIZ_PROTOCOL = {
|
|
|
2981
2981
|
var BIZ_CHANNEL = {
|
|
2982
2982
|
/** Scene (room-group) data. Hard-coded in the app, not part of `ChanIds`. */
|
|
2983
2983
|
SCENES: 0,
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
OBSTACLE_INFO: 8,
|
|
2992
|
-
MAP_DATA: 9,
|
|
2993
|
-
CRUISE_DATA: 10
|
|
2984
|
+
MAP_DATA: 2,
|
|
2985
|
+
ROOM_OUTLINE: 4,
|
|
2986
|
+
ROOM_PARAMS: 5,
|
|
2987
|
+
RESTRICTED_ZONE: 6,
|
|
2988
|
+
DYNAMIC_DATA: 7,
|
|
2989
|
+
MAP_DESCRIPTION: 60,
|
|
2990
|
+
MAP_BACKUP: 68
|
|
2994
2991
|
};
|
|
2995
2992
|
var BY_ID = new Map(Object.entries(BIZ_CHANNEL).map(([name, id]) => [id, name]));
|
|
2996
2993
|
function bizChannelName(id) {
|
|
@@ -6828,17 +6825,15 @@ var LIGHT_MEMBERS = {
|
|
|
6828
6825
|
writeAs: "setEnabled"
|
|
6829
6826
|
}
|
|
6830
6827
|
};
|
|
6828
|
+
var NO_SPOTLIGHT_MODELS = /* @__PURE__ */ new Set(["T8171", "T8416"]);
|
|
6831
6829
|
var LIGHT = {
|
|
6832
6830
|
capability: "light",
|
|
6833
6831
|
description: "Floodlight / spotlight manual switch and brightness.",
|
|
6834
6832
|
members: LIGHT_MEMBERS,
|
|
6835
6833
|
properties: propertiesOf(LIGHT_MEMBERS),
|
|
6836
|
-
// Three T8171 units on firmware 1.2.1.6 report 1400/1401/1403 without a spotlight, so those
|
|
6837
|
-
// param ids are not sufficient evidence for that model. The camera status LED (1045) is not light.
|
|
6838
|
-
// Other models keep the existing param and name evidence.
|
|
6839
6834
|
detection: {
|
|
6840
6835
|
modelHints: [/floodlight|wall.?light|spot.?light|search.?light/i],
|
|
6841
|
-
detect: (record) => record.model
|
|
6836
|
+
detect: (record) => !NO_SPOTLIGHT_MODELS.has(record.model ?? "") && [1400, 1401, 1403, 6080, 6082, 1413].some((param) => Object.hasOwn(record.params ?? {}, param))
|
|
6842
6837
|
},
|
|
6843
6838
|
/**
|
|
6844
6839
|
* Only what the member table cannot express: `on`/`off` as no-argument verbs over the same wire as the
|
|
@@ -8858,6 +8853,19 @@ function encodeSceneClean(sceneId) {
|
|
|
8858
8853
|
const { method: method2, param } = ModeCtrlParamMethod.SCENE;
|
|
8859
8854
|
return encodeModeCtrlParam(method2, param, (p) => p.int(SCENE_CLEAN_ID, sceneId));
|
|
8860
8855
|
}
|
|
8856
|
+
var MAP_WATCH_FIELD = {
|
|
8857
|
+
/** The time the write is sent, in microseconds since the epoch. */
|
|
8858
|
+
TIME_US: 1,
|
|
8859
|
+
/** `1` on the write that asks for the map. */
|
|
8860
|
+
WATCHING: 2
|
|
8861
|
+
};
|
|
8862
|
+
function encodeMapWatch(nowMs, watching) {
|
|
8863
|
+
return rawDp((w) => {
|
|
8864
|
+
w.int(MAP_WATCH_FIELD.TIME_US, Math.floor(nowMs) * 1e3);
|
|
8865
|
+
if (watching)
|
|
8866
|
+
w.int(MAP_WATCH_FIELD.WATCHING, 1);
|
|
8867
|
+
});
|
|
8868
|
+
}
|
|
8861
8869
|
function encodeAutoClean() {
|
|
8862
8870
|
const { method: method2, param } = ModeCtrlParamMethod.AUTO;
|
|
8863
8871
|
return encodeModeCtrlParam(method2, param, (p) => p.int(AUTO_CLEAN_TIMES, 1));
|
|
@@ -10727,6 +10735,20 @@ var VACUUM_CLEAN_MEMBERS = {
|
|
|
10727
10735
|
* WATCHED: run on a T2351, it started the named scene.
|
|
10728
10736
|
*/
|
|
10729
10737
|
startScene: method(({ sink }) => (sceneId) => sink.dispatch(aiotDp(VACUUM_DP.MODE_CTRL, encodeSceneClean(sceneId))), "Run a saved cleaning scene by its id (ModeCtrlRequest method 24 over DP 152).", isAiotVacuum),
|
|
10738
|
+
/**
|
|
10739
|
+
* Ask the robot to push its whole map — plane, room outline, room list and restricted zones — on the
|
|
10740
|
+
* map stream, as the vendor app does when its map view opens: two DP 153 writes, the time and then the
|
|
10741
|
+
* time with field 2 set (see {@link encodeMapWatch}).
|
|
10742
|
+
*
|
|
10743
|
+
* A docked robot sends no map of its own accord, so this is how its room ids and names become
|
|
10744
|
+
* readable without a clean. The map arrives on the stream like any other, not as this call's result.
|
|
10745
|
+
*
|
|
10746
|
+
* WATCHED: sent to a docked robot, it pushed its whole map within a second.
|
|
10747
|
+
*/
|
|
10748
|
+
requestMap: method(({ sink }) => async () => {
|
|
10749
|
+
await sink.dispatch(aiotDp(VACUUM_DP.WORK_STATUS, encodeMapWatch(Date.now(), false)));
|
|
10750
|
+
await sink.dispatch(aiotDp(VACUUM_DP.WORK_STATUS, encodeMapWatch(Date.now(), true)));
|
|
10751
|
+
}, "Ask the robot to push its whole map on the map stream (two map-watch writes over DP 153).", isAiotVacuum),
|
|
10730
10752
|
/**
|
|
10731
10753
|
* State the cleaning settings a run uses — `CleanParamRequest.clean_param` over DP 154.
|
|
10732
10754
|
*
|
|
@@ -15184,7 +15206,9 @@ var READER_BY_CHANNEL = {
|
|
|
15184
15206
|
ROOM_OUTLINE: reader("outline", decodeVacuumRoomOutline),
|
|
15185
15207
|
ROOM_PARAMS: reader("rooms", decodeVacuumRoomParams),
|
|
15186
15208
|
RESTRICTED_ZONE: reader("zones", decodeVacuumRestrictedZones),
|
|
15187
|
-
DYNAMIC_DATA: reader("pose", decodeVacuumPose)
|
|
15209
|
+
DYNAMIC_DATA: reader("pose", decodeVacuumPose),
|
|
15210
|
+
MAP_DESCRIPTION: reader("description", decodeVacuumMapDescription),
|
|
15211
|
+
MAP_BACKUP: reader("backup", decodeVacuumMapBackup)
|
|
15188
15212
|
};
|
|
15189
15213
|
var DECODED_MAP_CHANNELS = Object.keys(READER_BY_CHANNEL);
|
|
15190
15214
|
var READERS = new Map(DECODED_MAP_CHANNELS.map((name) => [BIZ_CHANNEL[name], READER_BY_CHANNEL[name]]));
|
|
@@ -16326,6 +16350,19 @@ function paramReport(payload) {
|
|
|
16326
16350
|
}
|
|
16327
16351
|
return Object.keys(out).length ? out : void 0;
|
|
16328
16352
|
}
|
|
16353
|
+
function channelReport(payload) {
|
|
16354
|
+
const list = payload?.params;
|
|
16355
|
+
if (!Array.isArray(list))
|
|
16356
|
+
return void 0;
|
|
16357
|
+
const out = {};
|
|
16358
|
+
for (const entry of list) {
|
|
16359
|
+
const p = entry;
|
|
16360
|
+
if (typeof p?.dev_type !== "number" || typeof p.param_type !== "number" || p.param_value == null)
|
|
16361
|
+
continue;
|
|
16362
|
+
(out[p.dev_type] ??= {})[p.param_type] = String(p.param_value);
|
|
16363
|
+
}
|
|
16364
|
+
return Object.keys(out).length ? out : void 0;
|
|
16365
|
+
}
|
|
16329
16366
|
var traceSequence = 0;
|
|
16330
16367
|
function firstTableRows(text2) {
|
|
16331
16368
|
const decoded = Buffer.from(text2, "latin1").toString("utf8");
|
|
@@ -16407,7 +16444,12 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16407
16444
|
audioStalled = false;
|
|
16408
16445
|
audioRetransmitTimer;
|
|
16409
16446
|
lastPongData;
|
|
16410
|
-
/**
|
|
16447
|
+
/**
|
|
16448
|
+
* When the selected peer last answered outbound P2P traffic on this connection.
|
|
16449
|
+
*
|
|
16450
|
+
* Each answer also emits `peerAnswer` with this time: a PONG or an ACK from the connected address is the
|
|
16451
|
+
* station replying on this connection now, which a heartbeat draws every {@link HEARTBEAT_MS}.
|
|
16452
|
+
*/
|
|
16411
16453
|
lastPeerAt;
|
|
16412
16454
|
/** Whether the silence has already been stated, so it is traced once per connection rather than per read. */
|
|
16413
16455
|
pathStaleTraced = false;
|
|
@@ -17046,6 +17088,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17046
17088
|
if (fromConnectedPeer && (hasHeader(msg, ResponseMessageType.PONG) || hasHeader(msg, ResponseMessageType.ACK))) {
|
|
17047
17089
|
this.lastPeerAt = Date.now();
|
|
17048
17090
|
this.pathStaleTraced = false;
|
|
17091
|
+
this.emit("peerAnswer", this.lastPeerAt);
|
|
17049
17092
|
}
|
|
17050
17093
|
if (!hasHeader(msg, ResponseMessageType.DATA)) {
|
|
17051
17094
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} <<< ${rinfo.address}:${rinfo.port} header=${msg.subarray(0, 2).toString("hex")} len=${msg.length}`);
|
|
@@ -18377,7 +18420,11 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
18377
18420
|
frame.json = JSON.parse(text2);
|
|
18378
18421
|
} catch {
|
|
18379
18422
|
}
|
|
18380
|
-
const
|
|
18423
|
+
const stationTable = header.commandId === CMD_CAMERA_INFO && header.channel === STATION_CHANNEL3;
|
|
18424
|
+
const byChannel = stationTable ? channelReport(frame.json) : void 0;
|
|
18425
|
+
const reported = header.commandId === CMD_NOTIFY_PAYLOAD ? paramReport(frame.json?.payload) : header.commandId === CMD_CAMERA_INFO && !stationTable ? paramReport(frame.json) : void 0;
|
|
18426
|
+
if (byChannel)
|
|
18427
|
+
frame.channelParams = byChannel;
|
|
18381
18428
|
if (reported)
|
|
18382
18429
|
frame.params = reported;
|
|
18383
18430
|
}
|
|
@@ -19496,6 +19543,7 @@ var LiveStream = class extends EventEmitter3 {
|
|
|
19496
19543
|
}
|
|
19497
19544
|
return void 0;
|
|
19498
19545
|
}
|
|
19546
|
+
/** Emit one accepted media frame; an audio frame with no bytes past its header emits nothing. */
|
|
19499
19547
|
onFrame(f) {
|
|
19500
19548
|
const accepted = this.acceptsMedia(f);
|
|
19501
19549
|
if (f.commandId === CMD_VIDEO_FRAME2 && !this.tracedFirstVideoCommand) {
|
|
@@ -19627,11 +19675,11 @@ var LiveStream = class extends EventEmitter3 {
|
|
|
19627
19675
|
* The payload of an audio frame. On a stream with an ECIES decoder, a sealed frame ({@link isSealedAudio}) is
|
|
19628
19676
|
* opened under the media key of the latest keyframe, and answers `undefined` when it does not open or arrives
|
|
19629
19677
|
* before any keyframe gave a media key, since its bytes are ciphertext. Every other frame is the bytes after
|
|
19630
|
-
* its header.
|
|
19678
|
+
* its header, empty for a frame that carries only its header.
|
|
19631
19679
|
*/
|
|
19632
19680
|
audioPayload(frame) {
|
|
19633
19681
|
if (!this.decoder || !isSealedAudio(frame)) {
|
|
19634
|
-
return frame.
|
|
19682
|
+
return frame.subarray(AUDIO_HEADER_LEN);
|
|
19635
19683
|
}
|
|
19636
19684
|
const mediaKey = this.decoder.currentMediaKey;
|
|
19637
19685
|
return mediaKey ? openSealedAudio(frame, mediaKey) : void 0;
|
|
@@ -22629,6 +22677,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22629
22677
|
this.tearDownStation(stationSn);
|
|
22630
22678
|
});
|
|
22631
22679
|
session.on("level2Ready", ({ cipherId }) => this.deps.onLevel2Ready(stationSn, cipherId));
|
|
22680
|
+
session.on("peerAnswer", (atMs) => {
|
|
22681
|
+
if (this.manager.get(stationSn) === session)
|
|
22682
|
+
this.deps.onPeerAnswer?.(stationSn, atMs);
|
|
22683
|
+
});
|
|
22632
22684
|
session.on("data", (f) => this.deps.onFrame(stationSn, f));
|
|
22633
22685
|
return session;
|
|
22634
22686
|
}
|
|
@@ -28844,12 +28896,15 @@ function mergeParams(raw, params, paramUpdatedAt) {
|
|
|
28844
28896
|
paramUpdatedAt[p.param_type] = ts;
|
|
28845
28897
|
}
|
|
28846
28898
|
}
|
|
28899
|
+
function newest(a, b) {
|
|
28900
|
+
return a === void 0 ? b : b === void 0 ? a : Math.max(a, b);
|
|
28901
|
+
}
|
|
28847
28902
|
function lastSeenMsOf(paramUpdatedAt) {
|
|
28848
|
-
let
|
|
28903
|
+
let newest2 = 0;
|
|
28849
28904
|
for (const ts of Object.values(paramUpdatedAt))
|
|
28850
|
-
if (ts >
|
|
28851
|
-
|
|
28852
|
-
return
|
|
28905
|
+
if (ts > newest2)
|
|
28906
|
+
newest2 = ts;
|
|
28907
|
+
return newest2 > 0 ? newest2 * 1e3 : void 0;
|
|
28853
28908
|
}
|
|
28854
28909
|
var CLASS_BY_CODEC = {
|
|
28855
28910
|
station: "homebase",
|
|
@@ -28971,6 +29026,7 @@ var DeviceRegistry = class {
|
|
|
28971
29026
|
this.onError(e);
|
|
28972
29027
|
}
|
|
28973
29028
|
const seen = /* @__PURE__ */ new Map();
|
|
29029
|
+
const prior = new Map(this.devices.map((d) => [d.sn, d.lastSeenMs]));
|
|
28974
29030
|
for (const body of bodies) {
|
|
28975
29031
|
let res;
|
|
28976
29032
|
try {
|
|
@@ -29005,7 +29061,7 @@ var DeviceRegistry = class {
|
|
|
29005
29061
|
p2pDid: raw.p2p_did,
|
|
29006
29062
|
params,
|
|
29007
29063
|
paramUpdatedAt,
|
|
29008
|
-
lastSeenMs: lastSeenMsOf(paramUpdatedAt),
|
|
29064
|
+
lastSeenMs: newest(lastSeenMsOf(paramUpdatedAt), prior.get(raw.device_sn)),
|
|
29009
29065
|
raw,
|
|
29010
29066
|
deviceClass,
|
|
29011
29067
|
...c
|
|
@@ -29241,6 +29297,16 @@ var DeviceRegistry = class {
|
|
|
29241
29297
|
return void 0;
|
|
29242
29298
|
}
|
|
29243
29299
|
}
|
|
29300
|
+
/**
|
|
29301
|
+
* Record that `sn` was heard on a realtime wire at `atMs`, advancing its {@link EufyDevice.lastSeenMs}.
|
|
29302
|
+
*
|
|
29303
|
+
* Only ever moves the time forward, and survives a {@link getDevices} rebuild.
|
|
29304
|
+
*/
|
|
29305
|
+
markSeen(sn, atMs) {
|
|
29306
|
+
const dev = this.devices.find((d) => d.sn === sn);
|
|
29307
|
+
if (dev)
|
|
29308
|
+
dev.lastSeenMs = newest(dev.lastSeenMs, atMs);
|
|
29309
|
+
}
|
|
29244
29310
|
/**
|
|
29245
29311
|
* Record state a device reported over its realtime wire, merging into whatever it last reported.
|
|
29246
29312
|
*
|
|
@@ -29255,9 +29321,7 @@ var DeviceRegistry = class {
|
|
|
29255
29321
|
if (!Object.keys(params).length)
|
|
29256
29322
|
return;
|
|
29257
29323
|
this.dpParams.set(sn, { ...this.dpParams.get(sn), ...params });
|
|
29258
|
-
|
|
29259
|
-
if (dev)
|
|
29260
|
-
dev.lastSeenMs = Date.now();
|
|
29324
|
+
this.markSeen(sn, Date.now());
|
|
29261
29325
|
this.deviceCapsCache.delete(sn);
|
|
29262
29326
|
const waiters = this.stateWaiters.get(sn);
|
|
29263
29327
|
if (waiters) {
|
|
@@ -29466,6 +29530,8 @@ var DEFAULT_PREWARM_TIERS = ["wired", "battery"];
|
|
|
29466
29530
|
var SESSION_RECYCLE_WAIT_MS = 5e3;
|
|
29467
29531
|
var PUSH_RETRY_MIN_MS = 3e4;
|
|
29468
29532
|
var PUSH_RETRY_MAX_MS = 6e5;
|
|
29533
|
+
var STATION_REOPEN_MIN_MS = 5e3;
|
|
29534
|
+
var STATION_REOPEN_MAX_MS = 3e5;
|
|
29469
29535
|
var RealtimeStartupSupersededError = class extends Error {
|
|
29470
29536
|
};
|
|
29471
29537
|
var RefreshWindowClosedError = class extends Error {
|
|
@@ -29572,6 +29638,12 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
29572
29638
|
pollTimer = new Timer();
|
|
29573
29639
|
/** Armed while a failed push bring-up waits to be retried; cancelled by {@link disconnect}. */
|
|
29574
29640
|
pushRetryTimer = new Timer();
|
|
29641
|
+
/**
|
|
29642
|
+
* Per wired station, the pending reopen of its session and the pause before the next one; see
|
|
29643
|
+
* {@link reopenWiredStation}. Dropped for a station when its session connects, and wholly by
|
|
29644
|
+
* {@link disconnect}.
|
|
29645
|
+
*/
|
|
29646
|
+
stationReopens = /* @__PURE__ */ new Map();
|
|
29575
29647
|
constructor(opts) {
|
|
29576
29648
|
super();
|
|
29577
29649
|
this.opts = opts;
|
|
@@ -29605,10 +29677,18 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
29605
29677
|
ensureDevices: async () => {
|
|
29606
29678
|
await this.registry.getDevices();
|
|
29607
29679
|
},
|
|
29608
|
-
onConnect: (sn) =>
|
|
29609
|
-
|
|
29680
|
+
onConnect: (sn) => {
|
|
29681
|
+
this.stationReopens.get(sn)?.timer.cancel();
|
|
29682
|
+
this.stationReopens.delete(sn);
|
|
29683
|
+
this.emit("p2pConnect", sn);
|
|
29684
|
+
},
|
|
29685
|
+
onClose: (sn) => {
|
|
29686
|
+
this.emit("p2pClose", sn);
|
|
29687
|
+
this.reopenWiredStation(sn);
|
|
29688
|
+
},
|
|
29610
29689
|
onError: (e) => this.reportError(e),
|
|
29611
29690
|
onLevel2Ready: (sn, cipherId) => this.emit("p2pLevel2Ready", { stationSn: sn, cipherId }),
|
|
29691
|
+
onPeerAnswer: (sn, atMs) => this.registry.markSeen(sn, atMs),
|
|
29612
29692
|
onFrame: (stationSn, f) => this.onP2PFrame(stationSn, f)
|
|
29613
29693
|
});
|
|
29614
29694
|
this.rtc = new RtcCommandRouter({
|
|
@@ -30081,7 +30161,16 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
30081
30161
|
releaseWrite.then(() => this.opts.logger?.debug(`[session ${sn}] recycle still waiting on an attached viewer \u2014 releasing the write that asked for it`))
|
|
30082
30162
|
]);
|
|
30083
30163
|
}
|
|
30084
|
-
/**
|
|
30164
|
+
/**
|
|
30165
|
+
* Reopen a wired station's session while the generation that warmed it is current: after
|
|
30166
|
+
* {@link recycleStandaloneSession} closed it, after it closed on its own, or after its warm-up failed.
|
|
30167
|
+
*
|
|
30168
|
+
* A wired station's session is the persistent one realtime start opened, so a station left without one
|
|
30169
|
+
* receives nothing until a command dials it again. The first reopen runs at once; one that fails is
|
|
30170
|
+
* retried after {@link STATION_REOPEN_MIN_MS}, doubling up to {@link STATION_REOPEN_MAX_MS}, until the
|
|
30171
|
+
* session connects or the generation ends. One reopen is pending per station at a time, and each failure
|
|
30172
|
+
* is reported once, by {@link P2PCommandRouter.ensureStation}.
|
|
30173
|
+
*/
|
|
30085
30174
|
reopenWiredStation(sn) {
|
|
30086
30175
|
const generation = this.realtimeGeneration;
|
|
30087
30176
|
if (generation?.epoch !== this.realtimeEpoch)
|
|
@@ -30090,9 +30179,18 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
30090
30179
|
return;
|
|
30091
30180
|
if (this.stationPower(sn) !== "wired")
|
|
30092
30181
|
return;
|
|
30093
|
-
|
|
30094
|
-
|
|
30095
|
-
|
|
30182
|
+
const reopen = this.stationReopens.get(sn) ?? { timer: new Timer(), delayMs: 0 };
|
|
30183
|
+
this.stationReopens.set(sn, reopen);
|
|
30184
|
+
if (reopen.timer.pending)
|
|
30185
|
+
return;
|
|
30186
|
+
reopen.timer.arm(reopen.delayMs, () => {
|
|
30187
|
+
if (generation.epoch !== this.realtimeEpoch)
|
|
30188
|
+
return;
|
|
30189
|
+
reopen.delayMs = Math.min(Math.max(reopen.delayMs * 2, STATION_REOPEN_MIN_MS), STATION_REOPEN_MAX_MS);
|
|
30190
|
+
void this.p2p.ensureStation(sn, generation.abort.signal).catch(() => {
|
|
30191
|
+
if (!generation.abort.signal.aborted && this.stationReopens.get(sn) === reopen)
|
|
30192
|
+
this.reopenWiredStation(sn);
|
|
30193
|
+
});
|
|
30096
30194
|
});
|
|
30097
30195
|
}
|
|
30098
30196
|
/**
|
|
@@ -30135,6 +30233,8 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
30135
30233
|
* fans same-kind sensors out by channel, so the resolved serial is folded in; a payload's own
|
|
30136
30234
|
* fields still win on conflict.
|
|
30137
30235
|
*
|
|
30236
|
+
* A station's camera-info table lands per channel, each channel's params on the device attached there.
|
|
30237
|
+
*
|
|
30138
30238
|
* Params the station volunteered land as device state first. They arrive in the cloud record's own
|
|
30139
30239
|
* `param_type → value` shape, so no capability has to claim an id it does not own, and the reads
|
|
30140
30240
|
* they back stop waiting for the next cloud poll. A capability decode follows for the frames that
|
|
@@ -30153,6 +30253,8 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
30153
30253
|
};
|
|
30154
30254
|
if (f.params)
|
|
30155
30255
|
this.applyRealtimeState(deviceSn, f.params);
|
|
30256
|
+
for (const [channel, params] of Object.entries(f.channelParams ?? {}))
|
|
30257
|
+
this.applyRealtimeState(this.registry.serialForFrame(stationSn, Number(channel)), params);
|
|
30156
30258
|
this.applyRealtimeReport(deviceSn, decodeState(signal, caps));
|
|
30157
30259
|
for (const ev of decodeEvent(signal, caps))
|
|
30158
30260
|
this.emitSemantic(ev.event, deviceSn ? { deviceSn, ...ev.payload } : ev.payload, {
|
|
@@ -31035,7 +31137,8 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
31035
31137
|
return this.rtc.keepWarm(route);
|
|
31036
31138
|
}
|
|
31037
31139
|
/** Eagerly open P2P sessions for WIRED stations only (persistent — they don't drain). Battery
|
|
31038
|
-
* stations stay closed until an on-demand open. Best-effort per station
|
|
31140
|
+
* stations stay closed until an on-demand open. Best-effort per station: one that fails is handed to
|
|
31141
|
+
* {@link reopenWiredStation}. */
|
|
31039
31142
|
async warmWiredP2P(readiness = createPlaneReadiness(), signal) {
|
|
31040
31143
|
const wired = /* @__PURE__ */ new Set();
|
|
31041
31144
|
for (const d of this.registry.p2pDevices()) {
|
|
@@ -31053,6 +31156,7 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
31053
31156
|
return;
|
|
31054
31157
|
readiness.failed++;
|
|
31055
31158
|
readiness.pending--;
|
|
31159
|
+
this.reopenWiredStation(sn);
|
|
31056
31160
|
})));
|
|
31057
31161
|
return readiness;
|
|
31058
31162
|
}
|
|
@@ -31727,6 +31831,9 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
31727
31831
|
async teardownRealtime() {
|
|
31728
31832
|
this.pollTimer.cancel();
|
|
31729
31833
|
this.pushRetryTimer.cancel();
|
|
31834
|
+
for (const { timer } of this.stationReopens.values())
|
|
31835
|
+
timer.cancel();
|
|
31836
|
+
this.stationReopens.clear();
|
|
31730
31837
|
this.lastStateAnnounced.clear();
|
|
31731
31838
|
await this.closeMqttTransports();
|
|
31732
31839
|
this.rtc.close();
|