@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.
@@ -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
- /** Reopen a wired station that {@link recycleStandaloneSession} closed, while the generation that warmed it is current. */
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
@@ -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 state is **not** a liveness signal and is not carried here: sessions are opened only
206
- * when something needs one and closed when idle, so "no session" is the resting state of a healthy
207
- * device. Transport visibility lives on `getP2pSessions()` and the `p2pConnect`/`p2pClose` events,
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 reported to the cloud, in ms. Bounded by
219
- * the cloud's own slow refresh — minutes, not seconds — so it answers "is this device alive at all",
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 reported to the cloud since the last poll — its {@link DeviceState.lastSeenMs} advanced.
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
@@ -80,8 +80,10 @@ export interface EufyDevice {
80
80
  */
81
81
  paramUpdatedAt?: Record<number, number>;
82
82
  /**
83
- * The most recent {@link paramUpdatedAt} across all of this device's params, in **milliseconds**
84
- * (comparable to `Date.now()` directly). `undefined` when the record carried no timestamp at all.
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
- MAP_INFO: 1,
2985
- PATH: 2,
2986
- ROOM_OUTLINE: 3,
2987
- ROOM_PARAMS: 4,
2988
- RESTRICTED_ZONE: 5,
2989
- DYNAMIC_DATA: 6,
2990
- TEMPORARY_DATA: 7,
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 !== "T8171" && [1400, 1401, 1403, 6080, 6082, 1413].some((param) => Object.hasOwn(record.params ?? {}, param))
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
- /** When the selected peer last answered outbound P2P traffic on this connection. */
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 reported = header.commandId === CMD_NOTIFY_PAYLOAD ? paramReport(frame.json?.payload) : header.commandId === CMD_CAMERA_INFO ? paramReport(frame.json) : void 0;
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 newest = 0;
28903
+ let newest2 = 0;
28877
28904
  for (const ts of Object.values(paramUpdatedAt))
28878
- if (ts > newest)
28879
- newest = ts;
28880
- return newest > 0 ? newest * 1e3 : void 0;
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
- const dev = this.devices.find((d) => d.sn === sn);
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) => this.emit("p2pConnect", sn),
29637
- onClose: (sn) => this.emit("p2pClose", sn),
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
- /** Reopen a wired station that {@link recycleStandaloneSession} closed, while the generation that warmed it is current. */
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
- void this.p2p.ensureStation(sn, generation.abort.signal).catch((error) => {
30122
- if (!generation.abort.signal.aborted)
30123
- this.reportError(error);
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();