@mega-yfue/eufy-sdk 0.6.0-beta.18 → 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 +111 -32
- package/dist/index.js.map +3 -3
- package/dist/transport/mqtt/biz-stream.d.ts +22 -25
- package/dist/transport/p2p/command-router.d.ts +2 -0
- 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
|
|
@@ -15211,7 +15206,9 @@ var READER_BY_CHANNEL = {
|
|
|
15211
15206
|
ROOM_OUTLINE: reader("outline", decodeVacuumRoomOutline),
|
|
15212
15207
|
ROOM_PARAMS: reader("rooms", decodeVacuumRoomParams),
|
|
15213
15208
|
RESTRICTED_ZONE: reader("zones", decodeVacuumRestrictedZones),
|
|
15214
|
-
DYNAMIC_DATA: reader("pose", decodeVacuumPose)
|
|
15209
|
+
DYNAMIC_DATA: reader("pose", decodeVacuumPose),
|
|
15210
|
+
MAP_DESCRIPTION: reader("description", decodeVacuumMapDescription),
|
|
15211
|
+
MAP_BACKUP: reader("backup", decodeVacuumMapBackup)
|
|
15215
15212
|
};
|
|
15216
15213
|
var DECODED_MAP_CHANNELS = Object.keys(READER_BY_CHANNEL);
|
|
15217
15214
|
var READERS = new Map(DECODED_MAP_CHANNELS.map((name) => [BIZ_CHANNEL[name], READER_BY_CHANNEL[name]]));
|
|
@@ -16353,6 +16350,19 @@ function paramReport(payload) {
|
|
|
16353
16350
|
}
|
|
16354
16351
|
return Object.keys(out).length ? out : void 0;
|
|
16355
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
|
+
}
|
|
16356
16366
|
var traceSequence = 0;
|
|
16357
16367
|
function firstTableRows(text2) {
|
|
16358
16368
|
const decoded = Buffer.from(text2, "latin1").toString("utf8");
|
|
@@ -16434,7 +16444,12 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16434
16444
|
audioStalled = false;
|
|
16435
16445
|
audioRetransmitTimer;
|
|
16436
16446
|
lastPongData;
|
|
16437
|
-
/**
|
|
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
|
+
*/
|
|
16438
16453
|
lastPeerAt;
|
|
16439
16454
|
/** Whether the silence has already been stated, so it is traced once per connection rather than per read. */
|
|
16440
16455
|
pathStaleTraced = false;
|
|
@@ -17073,6 +17088,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17073
17088
|
if (fromConnectedPeer && (hasHeader(msg, ResponseMessageType.PONG) || hasHeader(msg, ResponseMessageType.ACK))) {
|
|
17074
17089
|
this.lastPeerAt = Date.now();
|
|
17075
17090
|
this.pathStaleTraced = false;
|
|
17091
|
+
this.emit("peerAnswer", this.lastPeerAt);
|
|
17076
17092
|
}
|
|
17077
17093
|
if (!hasHeader(msg, ResponseMessageType.DATA)) {
|
|
17078
17094
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} <<< ${rinfo.address}:${rinfo.port} header=${msg.subarray(0, 2).toString("hex")} len=${msg.length}`);
|
|
@@ -18404,7 +18420,11 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
18404
18420
|
frame.json = JSON.parse(text2);
|
|
18405
18421
|
} catch {
|
|
18406
18422
|
}
|
|
18407
|
-
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;
|
|
18408
18428
|
if (reported)
|
|
18409
18429
|
frame.params = reported;
|
|
18410
18430
|
}
|
|
@@ -22657,6 +22677,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22657
22677
|
this.tearDownStation(stationSn);
|
|
22658
22678
|
});
|
|
22659
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
|
+
});
|
|
22660
22684
|
session.on("data", (f) => this.deps.onFrame(stationSn, f));
|
|
22661
22685
|
return session;
|
|
22662
22686
|
}
|
|
@@ -28872,12 +28896,15 @@ function mergeParams(raw, params, paramUpdatedAt) {
|
|
|
28872
28896
|
paramUpdatedAt[p.param_type] = ts;
|
|
28873
28897
|
}
|
|
28874
28898
|
}
|
|
28899
|
+
function newest(a, b) {
|
|
28900
|
+
return a === void 0 ? b : b === void 0 ? a : Math.max(a, b);
|
|
28901
|
+
}
|
|
28875
28902
|
function lastSeenMsOf(paramUpdatedAt) {
|
|
28876
|
-
let
|
|
28903
|
+
let newest2 = 0;
|
|
28877
28904
|
for (const ts of Object.values(paramUpdatedAt))
|
|
28878
|
-
if (ts >
|
|
28879
|
-
|
|
28880
|
-
return
|
|
28905
|
+
if (ts > newest2)
|
|
28906
|
+
newest2 = ts;
|
|
28907
|
+
return newest2 > 0 ? newest2 * 1e3 : void 0;
|
|
28881
28908
|
}
|
|
28882
28909
|
var CLASS_BY_CODEC = {
|
|
28883
28910
|
station: "homebase",
|
|
@@ -28999,6 +29026,7 @@ var DeviceRegistry = class {
|
|
|
28999
29026
|
this.onError(e);
|
|
29000
29027
|
}
|
|
29001
29028
|
const seen = /* @__PURE__ */ new Map();
|
|
29029
|
+
const prior = new Map(this.devices.map((d) => [d.sn, d.lastSeenMs]));
|
|
29002
29030
|
for (const body of bodies) {
|
|
29003
29031
|
let res;
|
|
29004
29032
|
try {
|
|
@@ -29033,7 +29061,7 @@ var DeviceRegistry = class {
|
|
|
29033
29061
|
p2pDid: raw.p2p_did,
|
|
29034
29062
|
params,
|
|
29035
29063
|
paramUpdatedAt,
|
|
29036
|
-
lastSeenMs: lastSeenMsOf(paramUpdatedAt),
|
|
29064
|
+
lastSeenMs: newest(lastSeenMsOf(paramUpdatedAt), prior.get(raw.device_sn)),
|
|
29037
29065
|
raw,
|
|
29038
29066
|
deviceClass,
|
|
29039
29067
|
...c
|
|
@@ -29269,6 +29297,16 @@ var DeviceRegistry = class {
|
|
|
29269
29297
|
return void 0;
|
|
29270
29298
|
}
|
|
29271
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
|
+
}
|
|
29272
29310
|
/**
|
|
29273
29311
|
* Record state a device reported over its realtime wire, merging into whatever it last reported.
|
|
29274
29312
|
*
|
|
@@ -29283,9 +29321,7 @@ var DeviceRegistry = class {
|
|
|
29283
29321
|
if (!Object.keys(params).length)
|
|
29284
29322
|
return;
|
|
29285
29323
|
this.dpParams.set(sn, { ...this.dpParams.get(sn), ...params });
|
|
29286
|
-
|
|
29287
|
-
if (dev)
|
|
29288
|
-
dev.lastSeenMs = Date.now();
|
|
29324
|
+
this.markSeen(sn, Date.now());
|
|
29289
29325
|
this.deviceCapsCache.delete(sn);
|
|
29290
29326
|
const waiters = this.stateWaiters.get(sn);
|
|
29291
29327
|
if (waiters) {
|
|
@@ -29494,6 +29530,8 @@ var DEFAULT_PREWARM_TIERS = ["wired", "battery"];
|
|
|
29494
29530
|
var SESSION_RECYCLE_WAIT_MS = 5e3;
|
|
29495
29531
|
var PUSH_RETRY_MIN_MS = 3e4;
|
|
29496
29532
|
var PUSH_RETRY_MAX_MS = 6e5;
|
|
29533
|
+
var STATION_REOPEN_MIN_MS = 5e3;
|
|
29534
|
+
var STATION_REOPEN_MAX_MS = 3e5;
|
|
29497
29535
|
var RealtimeStartupSupersededError = class extends Error {
|
|
29498
29536
|
};
|
|
29499
29537
|
var RefreshWindowClosedError = class extends Error {
|
|
@@ -29600,6 +29638,12 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
29600
29638
|
pollTimer = new Timer();
|
|
29601
29639
|
/** Armed while a failed push bring-up waits to be retried; cancelled by {@link disconnect}. */
|
|
29602
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();
|
|
29603
29647
|
constructor(opts) {
|
|
29604
29648
|
super();
|
|
29605
29649
|
this.opts = opts;
|
|
@@ -29633,10 +29677,18 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
29633
29677
|
ensureDevices: async () => {
|
|
29634
29678
|
await this.registry.getDevices();
|
|
29635
29679
|
},
|
|
29636
|
-
onConnect: (sn) =>
|
|
29637
|
-
|
|
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
|
+
},
|
|
29638
29689
|
onError: (e) => this.reportError(e),
|
|
29639
29690
|
onLevel2Ready: (sn, cipherId) => this.emit("p2pLevel2Ready", { stationSn: sn, cipherId }),
|
|
29691
|
+
onPeerAnswer: (sn, atMs) => this.registry.markSeen(sn, atMs),
|
|
29640
29692
|
onFrame: (stationSn, f) => this.onP2PFrame(stationSn, f)
|
|
29641
29693
|
});
|
|
29642
29694
|
this.rtc = new RtcCommandRouter({
|
|
@@ -30109,7 +30161,16 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
30109
30161
|
releaseWrite.then(() => this.opts.logger?.debug(`[session ${sn}] recycle still waiting on an attached viewer \u2014 releasing the write that asked for it`))
|
|
30110
30162
|
]);
|
|
30111
30163
|
}
|
|
30112
|
-
/**
|
|
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
|
+
*/
|
|
30113
30174
|
reopenWiredStation(sn) {
|
|
30114
30175
|
const generation = this.realtimeGeneration;
|
|
30115
30176
|
if (generation?.epoch !== this.realtimeEpoch)
|
|
@@ -30118,9 +30179,18 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
30118
30179
|
return;
|
|
30119
30180
|
if (this.stationPower(sn) !== "wired")
|
|
30120
30181
|
return;
|
|
30121
|
-
|
|
30122
|
-
|
|
30123
|
-
|
|
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
|
+
});
|
|
30124
30194
|
});
|
|
30125
30195
|
}
|
|
30126
30196
|
/**
|
|
@@ -30163,6 +30233,8 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
30163
30233
|
* fans same-kind sensors out by channel, so the resolved serial is folded in; a payload's own
|
|
30164
30234
|
* fields still win on conflict.
|
|
30165
30235
|
*
|
|
30236
|
+
* A station's camera-info table lands per channel, each channel's params on the device attached there.
|
|
30237
|
+
*
|
|
30166
30238
|
* Params the station volunteered land as device state first. They arrive in the cloud record's own
|
|
30167
30239
|
* `param_type → value` shape, so no capability has to claim an id it does not own, and the reads
|
|
30168
30240
|
* they back stop waiting for the next cloud poll. A capability decode follows for the frames that
|
|
@@ -30181,6 +30253,8 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
30181
30253
|
};
|
|
30182
30254
|
if (f.params)
|
|
30183
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);
|
|
30184
30258
|
this.applyRealtimeReport(deviceSn, decodeState(signal, caps));
|
|
30185
30259
|
for (const ev of decodeEvent(signal, caps))
|
|
30186
30260
|
this.emitSemantic(ev.event, deviceSn ? { deviceSn, ...ev.payload } : ev.payload, {
|
|
@@ -31063,7 +31137,8 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
31063
31137
|
return this.rtc.keepWarm(route);
|
|
31064
31138
|
}
|
|
31065
31139
|
/** Eagerly open P2P sessions for WIRED stations only (persistent — they don't drain). Battery
|
|
31066
|
-
* 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}. */
|
|
31067
31142
|
async warmWiredP2P(readiness = createPlaneReadiness(), signal) {
|
|
31068
31143
|
const wired = /* @__PURE__ */ new Set();
|
|
31069
31144
|
for (const d of this.registry.p2pDevices()) {
|
|
@@ -31081,6 +31156,7 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
31081
31156
|
return;
|
|
31082
31157
|
readiness.failed++;
|
|
31083
31158
|
readiness.pending--;
|
|
31159
|
+
this.reopenWiredStation(sn);
|
|
31084
31160
|
})));
|
|
31085
31161
|
return readiness;
|
|
31086
31162
|
}
|
|
@@ -31755,6 +31831,9 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
31755
31831
|
async teardownRealtime() {
|
|
31756
31832
|
this.pollTimer.cancel();
|
|
31757
31833
|
this.pushRetryTimer.cancel();
|
|
31834
|
+
for (const { timer } of this.stationReopens.values())
|
|
31835
|
+
timer.cancel();
|
|
31836
|
+
this.stationReopens.clear();
|
|
31758
31837
|
this.lastStateAnnounced.clear();
|
|
31759
31838
|
await this.closeMqttTransports();
|
|
31760
31839
|
this.rtc.close();
|