@mega-yfue/eufy-sdk 0.2.0-beta.1 → 0.2.0-beta.3

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.
@@ -74,6 +74,61 @@ export declare class CameraDisabledError extends Error {
74
74
  cause?: unknown;
75
75
  });
76
76
  }
77
+ /**
78
+ * Work on a station was refused: the station did not provide the session key that work requires.
79
+ *
80
+ * A station reached over its HomeBase encrypts what it is sent under a key negotiated once per connection, and
81
+ * a media start for an attached camera has no unencrypted form at all — so without that key there is nothing
82
+ * to send, however reachable the station is. Naming this apart from a source that failed is what separates an
83
+ * account whose cipher material could not be resolved from a camera that is off, a station that is busy, or a
84
+ * stream that produced nothing: they share no next step.
85
+ *
86
+ * The `level2-unavailable` trace states WHY the key is not coming. This states only that it is not, because
87
+ * that is what the refusal itself knows.
88
+ *
89
+ * `stationSn` is the station that owed the key, which is the parent for an attached camera and therefore not
90
+ * the serial the refused call was made about: several cameras refused at once are one station's outcome, and
91
+ * nothing else in the refusal says so.
92
+ */
93
+ export declare class StationKeyUnavailableError extends Error {
94
+ /** The station whose session key did not arrive. */
95
+ readonly stationSn: string;
96
+ /** Always true: the negotiation is per connection, so a later one may still produce a key. */
97
+ readonly retryable = true;
98
+ constructor(
99
+ /** The station whose session key did not arrive. */
100
+ stationSn: string, options?: {
101
+ cause?: unknown;
102
+ });
103
+ }
104
+ /**
105
+ * Work on a station was refused: its session did not connect within the wait it was given.
106
+ *
107
+ * A station is reached over its own session, and nothing addressed to it — a media start, a property read, a
108
+ * still — can be attempted before that session is up. Naming this apart from every other failure is what tells
109
+ * a station that could not be reached at all from one that answered and then refused, or one that served media
110
+ * a caller could not use: those call for opposite next steps, and a caller cannot infer which it had from a
111
+ * message.
112
+ *
113
+ * `waitedMs` is how long was actually waited, which a caller compares against its own deadline to know whether
114
+ * this SDK concluded or its own bound expired first. `stationSn` is the station that could not be reached —
115
+ * the parent for an attached camera, so it is not derivable from the serial the call was made about.
116
+ */
117
+ export declare class StationUnreachableError extends Error {
118
+ /** The station whose session did not connect. */
119
+ readonly stationSn: string;
120
+ /** How long the session was waited on before this was raised. */
121
+ readonly waitedMs: number;
122
+ /** Always true: a station unreachable now may answer on a later attempt. */
123
+ readonly retryable = true;
124
+ constructor(
125
+ /** The station whose session did not connect. */
126
+ stationSn: string,
127
+ /** How long the session was waited on before this was raised. */
128
+ waitedMs: number, options?: {
129
+ cause?: unknown;
130
+ });
131
+ }
77
132
  /**
78
133
  * A live stream was refused: the station is already serving another of its cameras to a viewer.
79
134
  *
package/dist/index.js CHANGED
@@ -2556,6 +2556,28 @@ var CameraDisabledError = class extends Error {
2556
2556
  this.name = "CameraDisabledError";
2557
2557
  }
2558
2558
  };
2559
+ var StationKeyUnavailableError = class extends Error {
2560
+ stationSn;
2561
+ /** Always true: the negotiation is per connection, so a later one may still produce a key. */
2562
+ retryable = true;
2563
+ constructor(stationSn, options) {
2564
+ super(`station ${stationSn} did not provide its session key, so nothing that requires one could be sent`, options);
2565
+ this.stationSn = stationSn;
2566
+ this.name = "StationKeyUnavailableError";
2567
+ }
2568
+ };
2569
+ var StationUnreachableError = class extends Error {
2570
+ stationSn;
2571
+ waitedMs;
2572
+ /** Always true: a station unreachable now may answer on a later attempt. */
2573
+ retryable = true;
2574
+ constructor(stationSn, waitedMs, options) {
2575
+ super(`station ${stationSn}'s P2P session did not connect within ${waitedMs}ms, so nothing could be sent to it`, options);
2576
+ this.stationSn = stationSn;
2577
+ this.waitedMs = waitedMs;
2578
+ this.name = "StationUnreachableError";
2579
+ }
2580
+ };
2559
2581
  var StationBusyError = class extends Error {
2560
2582
  servingChannel;
2561
2583
  /** Always true: the station is busy now, and stops being busy when the other stream is released. */
@@ -6890,6 +6912,16 @@ var KEYPAD = {
6890
6912
  // dist/model/capabilities/arming.js
6891
6913
  var STATION_CHANNEL2 = 255;
6892
6914
  var ArmingMode = {
6915
+ /** Armed — full protection, nobody home (wire value 0). */
6916
+ away: "away",
6917
+ /** Armed for occupancy — reduced/perimeter protection while home (wire value 1). */
6918
+ home: "home",
6919
+ /** Custom 1 — a user-defined posture configured in the app (wire value 3). */
6920
+ custom1: "custom1",
6921
+ /** Disarmed — no alarms; sensors still report state (wire value 63). */
6922
+ disarmed: "disarmed"
6923
+ };
6924
+ var AlarmDelayMode = {
6893
6925
  /** Armed — full protection, nobody home (wire value 0). */
6894
6926
  away: "away",
6895
6927
  /** Armed for occupancy — reduced/perimeter protection while home (wire value 1). */
@@ -6905,10 +6937,11 @@ var ARMING_CMD = {
6905
6937
  * mValue3:0, `payload:{mode_type:<int>, user_name:<string>}`.
6906
6938
  *
6907
6939
  * ⚠️ Only 3 of the 9 modes were exercised in that capture — `mode_type` 0 (away), 63 (disarmed), 1
6908
- * (home), all confirmed byte-exact, and those three are the whole of {@link ArmingMode}. Re-confirmed
6909
- * live 2026-08-05: each reported its own MODE_SWITCH push within ~5s of the write. The remaining six are
6910
- * named by the app but never observed leaving it, so this capability reads them and refuses to send
6911
- * them. See `ARMING_MODE_WIRE` for the per-value breakdown.
6940
+ * (home), all confirmed byte-exact. Re-confirmed live 2026-08-05: each reported its own MODE_SWITCH push
6941
+ * within ~5s of the write. `custom1` 3 joined {@link ArmingMode} on a live confirmation rather than a
6942
+ * capture, making four settable in total. The remaining five are named by the app but never observed
6943
+ * leaving it, so this capability reads them and refuses to send them. See `ARMING_MODE_WIRE` for the
6944
+ * per-value breakdown.
6912
6945
  */
6913
6946
  SET_ARMING: 1224,
6914
6947
  /**
@@ -6944,7 +6977,6 @@ var ARMING_MODE_WIRE = {
6944
6977
  schedule: 2,
6945
6978
  // ⚠️ reportable, NOT settable — see the doc comment above
6946
6979
  custom1: 3,
6947
- // ⚠️ reportable, NOT settable — see the doc comment above
6948
6980
  custom2: 4,
6949
6981
  // ⚠️ reportable, NOT settable — see the doc comment above
6950
6982
  custom3: 5,
@@ -6987,9 +7019,9 @@ function alarmDelayCommand(mode, config, ctx) {
6987
7019
  var ARMING_MEMBERS = {
6988
7020
  /**
6989
7021
  * The one member whose write domain is NARROWER than its read: `enumValues` names all nine modes a
6990
- * station can report, and the argument's `values` publishes only the three whose wire was captured. That
6991
- * argument IS the domain the derived setter enforces and the refusal names, so an uncaptured mode is
6992
- * refused by naming the three that work — nine labels for the read and three for the write, off one
7022
+ * station can report, and the argument's `values` publishes only the four whose write is confirmed. That
7023
+ * argument IS the domain the derived setter enforces and the refusal names, so an unconfirmed mode is
7024
+ * refused by naming the four that work — nine labels for the read and four for the write, off one
6993
7025
  * declaration.
6994
7026
  *
6995
7027
  * `armingCommand` may also throw synchronously (missing account identity) and `bindMembers` turns that
@@ -7010,7 +7042,7 @@ var ARMING_MEMBERS = {
7010
7042
  enumValues: ARMING_MODE_LABELS,
7011
7043
  provenance: "verified",
7012
7044
  args: [{ name: "mode", kind: "enum", values: SETTABLE_MODES }],
7013
- description: "Guard mode (verified: param 1224 = GUARD_MODE, read/write mechanism confirmed). Reads all 9 modes the app defines; SETS only the 3 whose write is wire-captured (away/home/disarmed) \u2014 schedule/custom1/custom2/custom3/off/geo are named by the app but no capture shows one being sent, so they are refused rather than guessed; see ARMING_MODE_WIRE in arming.ts for the breakdown.",
7045
+ description: "Guard mode (verified: param 1224 = GUARD_MODE, read/write mechanism confirmed). Reads all 9 modes the app defines; SETS only the 4 whose write is confirmed (away/home/custom1/disarmed) \u2014 schedule/custom2/custom3/off/geo are named by the app but no capture shows one being sent, so they are refused rather than guessed; see ARMING_MODE_WIRE in arming.ts for the breakdown.",
7014
7046
  observation: {
7015
7047
  event: "armingModeChanged",
7016
7048
  reflects: (value) => ({ param: ARMING_CMD.SET_ARMING, expected: ARMING_MODE_WIRE[armingModeOf(value)] }),
@@ -7031,9 +7063,10 @@ var ARMING_MEMBERS = {
7031
7063
  * this mode — there is no known GET to fetch it automatically, and a wrong guess here can silently
7032
7064
  * misconfigure which sensors arm/trigger for real.
7033
7065
  *
7034
- * Takes {@link ArmingMode}, so a delay can only be configured for a mode whose `mode_id` integer is
7035
- * captured. The frame carries that same integer, so a schedule/custom mode would be the identical guess
7036
- * `setMode` refuses.
7066
+ * Takes {@link AlarmDelayMode}, not {@link ArmingMode}: a delay is configurable only for a mode whose
7067
+ * integer is captured on THIS command, and `custom1` is confirmed on cmd 1224 only. The frame carries
7068
+ * that integer in `mode_id` with no runtime validation and no readback, so a mode outside this union
7069
+ * would be the same unverified guess `setMode` refuses.
7037
7070
  */
7038
7071
  setAlarmDelayConfig: method(({ ctx, sink }) => (mode, config) => {
7039
7072
  try {
@@ -14190,19 +14223,27 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
14190
14223
  * cameras from that second group streamed normally at level-1 — including one of the same firmware as an
14191
14224
  * own-session camera that delivered no video at all for a reason of its own. An expired grace therefore
14192
14225
  * separates nothing on this path, and a start failure on such a session is not evidence about it.
14226
+ *
14227
+ * Every `false` answer carries a `level2-unavailable` trace naming its reason, wherever the wait ended: a
14228
+ * `terminal` outcome is the one already stated where the negotiation concluded, since that is where the
14229
+ * cipher and the cause are known, and re-stating it here would double every settled negotiation.
14193
14230
  */
14194
14231
  async awaitLevel2Key(graceMs, graceFrom = "call") {
14195
- if (this.closed)
14232
+ if (this.closed) {
14233
+ this.trace({ phase: "level2-unavailable", reason: "session-closed" });
14196
14234
  return false;
14235
+ }
14197
14236
  if (this.level2Key)
14198
14237
  return true;
14199
- if (!this.level2Pending)
14238
+ if (!this.level2Pending) {
14239
+ this.trace({ phase: "level2-unavailable", reason: "not-negotiating" });
14200
14240
  return false;
14241
+ }
14201
14242
  const since = graceFrom === "call" ? Date.now() : this.connectedAtMs ?? Date.now();
14202
14243
  const remaining = graceMs - (Date.now() - since);
14203
14244
  if (remaining <= 0) {
14204
14245
  this.logger.debug(`[p2p] ${this.cfg.stationSn} no level-2 key and its ${graceMs}ms grace has elapsed`);
14205
- this.trace({ phase: "level2-absent", waitedMs: graceMs });
14246
+ this.trace({ phase: "level2-unavailable", reason: "grace-elapsed", waitedMs: graceMs });
14206
14247
  return false;
14207
14248
  }
14208
14249
  this.logger.debug(`[p2p] ${this.cfg.stationSn} waiting up to ${remaining}ms for the level-2 key`);
@@ -14228,10 +14269,12 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
14228
14269
  });
14229
14270
  if (outcome === "timeout") {
14230
14271
  this.logger.debug(`[p2p] ${this.cfg.stationSn} level-2 key did not arrive within its grace`);
14272
+ this.trace({ phase: "level2-unavailable", reason: "grace-elapsed", waitedMs: remaining });
14231
14273
  } else if (outcome === "terminal") {
14232
14274
  this.logger.debug(`[p2p] ${this.cfg.stationSn} level-2 negotiation concluded without a key`);
14233
14275
  } else if (outcome === "closed") {
14234
14276
  this.logger.debug(`[p2p] ${this.cfg.stationSn} session closed before the level-2 key arrived`);
14277
+ this.trace({ phase: "level2-unavailable", reason: "session-closed" });
14235
14278
  }
14236
14279
  return outcome === "key";
14237
14280
  }
@@ -14282,6 +14325,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
14282
14325
  this.level2Negotiating = true;
14283
14326
  const generation = this.connectionGeneration;
14284
14327
  const cipherId = gatewayInfoCipherId(gwPayload);
14328
+ this.trace({ phase: "level2-negotiating", cipherId });
14285
14329
  void (async () => {
14286
14330
  try {
14287
14331
  const eccPrivHex = await this.cfg.resolveCipherKey?.(cipherId);
@@ -14289,11 +14333,13 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
14289
14333
  return;
14290
14334
  if (!eccPrivHex) {
14291
14335
  this.logger.debug(`[p2p] ${this.cfg.stationSn} no ECC key for cipher_id ${cipherId}`);
14336
+ this.trace({ phase: "level2-unavailable", reason: "no-cipher-key", cipherId });
14292
14337
  this.settleLevel2();
14293
14338
  return;
14294
14339
  }
14295
14340
  const key = deriveLevel2KeyFromGatewayInfo(gwPayload, eccPrivHex);
14296
14341
  if (!key) {
14342
+ this.trace({ phase: "level2-unavailable", reason: "derivation-failed", cipherId });
14297
14343
  this.settleLevel2();
14298
14344
  this.emit("error", new Error(`level-2 key derivation failed (cipher_id ${cipherId})`));
14299
14345
  return;
@@ -14305,6 +14351,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
14305
14351
  } catch (e) {
14306
14352
  if (this.closed || generation !== this.connectionGeneration)
14307
14353
  return;
14354
+ this.trace({ phase: "level2-unavailable", reason: "derivation-failed", cipherId });
14308
14355
  this.settleLevel2();
14309
14356
  this.emit("error", e instanceof Error ? e : new Error(String(e)));
14310
14357
  }
@@ -18858,6 +18905,11 @@ function abortable(work, signal) {
18858
18905
  }
18859
18906
  var LEVEL2_GRACE_MS = 25e3;
18860
18907
  var LEVEL2_SETTLE_MS = 8e3;
18908
+ var P2P_STATION_WAITS = {
18909
+ connect: CONNECT_WAIT_MS,
18910
+ level2Grace: LEVEL2_GRACE_MS,
18911
+ level2Settle: LEVEL2_SETTLE_MS
18912
+ };
18861
18913
  var RTSP_URL_READ_TIMEOUT_MS = 12e3;
18862
18914
  var SHARED_LIVE_OPT_KEYS = [
18863
18915
  "eccPrivateKey",
@@ -18909,6 +18961,14 @@ var P2PCommandRouter = class _P2PCommandRouter {
18909
18961
  }
18910
18962
  return normalized;
18911
18963
  }
18964
+ /**
18965
+ * Emit a live trace under a station session's handle, for work this router does ON that session before
18966
+ * the session itself records anything — reaching the station, and resolving what a device is on it. Same
18967
+ * handle as everything the session goes on to trace, which is what groups one attempt.
18968
+ */
18969
+ traceOnStation(session, trace) {
18970
+ traceLiveStart(this.deps.logger ?? noopLogger, trace, session.traceId);
18971
+ }
18912
18972
  /**
18913
18973
  * Whether this transport stack drives `dev`'s `ff09-*` commands — true when the device has its own
18914
18974
  * usable P2P endpoint (a non-empty `p2p_did`). The command sink asks each stack this to route a
@@ -19049,7 +19109,15 @@ var P2PCommandRouter = class _P2PCommandRouter {
19049
19109
  let ecc;
19050
19110
  try {
19051
19111
  const ciphers = await this.deps.mega.getCiphers([cipherId], adminUserId, stationSn);
19052
- ecc = ciphers.find((c) => Number(c.cipher_id) === cipherId)?.ecc_private_key ?? ciphers[0]?.ecc_private_key;
19112
+ ecc = ciphers.find((c) => Number(c.cipher_id) === cipherId)?.ecc_private_key;
19113
+ if (ecc === void 0 && ciphers[0]?.ecc_private_key !== void 0) {
19114
+ ecc = ciphers[0].ecc_private_key;
19115
+ this.traceOnStation(session, {
19116
+ phase: "cipher-fallback",
19117
+ cipherId,
19118
+ answeredCipherId: Number(ciphers[0].cipher_id)
19119
+ });
19120
+ }
19053
19121
  } catch (e) {
19054
19122
  this.deps.onError(e instanceof Error ? e : new Error(String(e)));
19055
19123
  }
@@ -19680,9 +19748,9 @@ var P2PCommandRouter = class _P2PCommandRouter {
19680
19748
  s.sendStringPayloadCommand(P2P_ENVELOPE.CONTROL_PAYLOAD, json, ch);
19681
19749
  return Promise.resolve();
19682
19750
  },
19683
- l2: async ({ session: s, channel: ch }) => {
19751
+ l2: async ({ session: s, channel: ch, parentSn }) => {
19684
19752
  if (!await s.awaitLevel2Key(LEVEL2_GRACE_MS, "call")) {
19685
- throw new Error(`level-2 key not ready for ${sn} \u2014 cannot query`);
19753
+ throw new StationKeyUnavailableError(parentSn);
19686
19754
  }
19687
19755
  s.sendRawLevel2(json, ch, P2P_ENVELOPE.CONTROL_PAYLOAD);
19688
19756
  }
@@ -19849,15 +19917,28 @@ var P2PCommandRouter = class _P2PCommandRouter {
19849
19917
  }
19850
19918
  this.manager.bumpCommand(parentSn);
19851
19919
  const channel = typeof raw.device_channel === "number" ? raw.device_channel : 0;
19852
- const accountId = raw.member?.admin_user_id ?? this.deps.mega.auth?.userId ?? "";
19920
+ const stationAdminId = raw.member?.admin_user_id;
19921
+ const accountId = stationAdminId ?? this.deps.mega.auth?.userId ?? "";
19853
19922
  const t0 = Date.now();
19854
- while (!session.isConnected && Date.now() - t0 < CONNECT_WAIT_MS) {
19855
- opts.signal?.throwIfAborted();
19856
- await sleep2(200);
19923
+ let waitedMs = 0;
19924
+ if (!session.isConnected) {
19925
+ this.traceOnStation(session, { phase: "session-connect-wait", waitMs: CONNECT_WAIT_MS });
19926
+ while (!session.isConnected && Date.now() - t0 < CONNECT_WAIT_MS) {
19927
+ opts.signal?.throwIfAborted();
19928
+ await sleep2(200);
19929
+ }
19930
+ waitedMs = Date.now() - t0;
19931
+ this.traceOnStation(session, session.isConnected ? { phase: "session-connected", waitedMs } : { phase: "session-unreachable", waitedMs });
19857
19932
  }
19858
19933
  opts.signal?.throwIfAborted();
19859
19934
  if (!session.isConnected)
19860
- throw new Error(`P2P session for ${parentSn} did not connect`);
19935
+ throw new StationUnreachableError(parentSn, waitedMs);
19936
+ this.traceOnStation(session, {
19937
+ phase: "station-resolved",
19938
+ topology: homeBaseAttached ? "attached" : "own",
19939
+ channel,
19940
+ stationAdmin: typeof stationAdminId !== "string" ? "unstated" : stationAdminId === this.deps.mega.auth?.userId ? "self" : "other"
19941
+ });
19861
19942
  if (opts.waitLevel2) {
19862
19943
  if (opts.waitLevel2 === "settle") {
19863
19944
  await abortable(session.awaitLevel2Key(LEVEL2_SETTLE_MS, "session"), opts.signal);
@@ -19871,7 +19952,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
19871
19952
  ready = await abortable(session.awaitLevel2Key(LEVEL2_GRACE_MS, "call"), opts.signal);
19872
19953
  }
19873
19954
  if (!ready)
19874
- throw new Error(`level-2 key not ready for ${parentSn}`);
19955
+ throw new StationKeyUnavailableError(parentSn);
19875
19956
  }
19876
19957
  return { session, parentSn, channel, accountId, homeBaseAttached };
19877
19958
  }
@@ -25312,6 +25393,7 @@ export {
25312
25393
  AUDIO_MEMBERS,
25313
25394
  AccessUnitAssembler,
25314
25395
  AiDetectType,
25396
+ AlarmDelayMode,
25315
25397
  ArmingMode,
25316
25398
  BATTERY_MEMBERS,
25317
25399
  BIZ_CHANNEL,
@@ -25385,6 +25467,7 @@ export {
25385
25467
  P256,
25386
25468
  P2PSession,
25387
25469
  P2P_ENVELOPE,
25470
+ P2P_STATION_WAITS,
25388
25471
  PRINTER_CATEGORY_RE,
25389
25472
  PTZ_MEMBERS,
25390
25473
  PowerSource,
@@ -25418,6 +25501,8 @@ export {
25418
25501
  SmartDropPushEvent,
25419
25502
  StateConvergenceError,
25420
25503
  StationBusyError,
25504
+ StationKeyUnavailableError,
25505
+ StationUnreachableError,
25421
25506
  StoredSnapshotUnavailableError,
25422
25507
  StreamingQuality,
25423
25508
  SuctionLevel,