@mega-yfue/eufy-sdk 0.2.0-beta.20 → 0.2.0-beta.22

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.
@@ -481,7 +481,7 @@ export declare function buildActions(caps: readonly Capability[], deps: MemberDe
481
481
  * @internal
482
482
  */
483
483
  export declare function describeCapabilities(bound: Partial<DeviceActionMap>, ctx?: AvailabilityContext): CapabilityDescriptor[];
484
- export type { CommandContext, CapabilityActions, CapabilityStateReader, CapabilityModule, DetectionSpec, CapabilityFrame, CapabilityEvent, InboundSignal, EventMapping, ProductLine, ActionArgSpec, DecodedState, } from "./types.js";
484
+ export type { CommandContext, CapabilityActions, CapabilityStateReader, CapabilityModule, DetectionSpec, CapabilityFrame, CapabilityEvent, InboundSignal, EventClaim, EventMapping, ProductLine, ActionArgSpec, DecodedState, } from "./types.js";
485
485
  /**
486
486
  * The value-kind vocabulary a read is annotated with, re-exported from the barrel that publishes the
487
487
  * read itself, so the union a member's `kind` is drawn from is reachable from the same import.
@@ -136,6 +136,45 @@ export interface EventMapping {
136
136
  * capture. Return `{}` when this signal doesn't carry the field, so nothing is invented.
137
137
  */
138
138
  derive?(signal: InboundSignal): Record<string, unknown>;
139
+ /**
140
+ * Evidence that this device deals in the classification the id names, for an id whose presence in
141
+ * the wire vocabulary does not prove it.
142
+ *
143
+ * The AI-detection ids are shared verbatim across the camera families, so the id space says what an
144
+ * integer MEANS and never which units classify that way. A claim is how a mapping states the
145
+ * evidence that separates them, and it narrows the DESCRIPTION only: {@link EventMapping} stays in
146
+ * the dispatch index unclaimed, so a device that sends the push still gets the event. Under-reporting
147
+ * what a device is expected to emit is recoverable; dropping an event it did emit is not.
148
+ *
149
+ * Every field must hold — the AND to {@link DetectionSpec}'s OR, because a claim rules a family OUT
150
+ * rather than finding one more reason to say yes. A fact the context does not carry rules nothing
151
+ * out: only evidence that positively contradicts the claim withdraws the event.
152
+ */
153
+ claim?: EventClaim;
154
+ }
155
+ /**
156
+ * Evidence separating the families that share one inbound id.
157
+ *
158
+ * Both fields are optional and independent; an empty claim asserts nothing and is the same as none.
159
+ */
160
+ export interface EventClaim {
161
+ /**
162
+ * The codecs whose devices issue the id. For an id drawn from a vocabulary one device family owns:
163
+ * the AI-detection ids belong to the camera families, and a standalone sensor announces its own
164
+ * motion under a different id entirely.
165
+ */
166
+ codecs?: readonly Codec[];
167
+ /**
168
+ * Member names whose INSTALLED getter is the evidence — the device reported the parameter behind
169
+ * the classification, which is the same bar every typed read is held to.
170
+ */
171
+ reads?: readonly string[];
172
+ /**
173
+ * The topology the id belongs to: `true` for an id only a station's attached device sends, `false`
174
+ * for one only a standalone unit sends. Compared against {@link AvailabilityContext.homeBaseAttached},
175
+ * and ignored where that is absent.
176
+ */
177
+ homeBaseAttached?: boolean;
139
178
  }
140
179
  /**
141
180
  * A decoded inbound event a capability wants surfaced on the SDK. `event` is the EufyMega event
@@ -197,6 +236,15 @@ export interface AvailabilityContext {
197
236
  * `undefined` as an empty set — `ctx.paramIds?.has(dp) ?? false`.
198
237
  */
199
238
  paramIds?: ReadonlySet<number>;
239
+ /**
240
+ * Whether the device hangs off a HomeBase (a `parent_sn` other than its own) rather than standing
241
+ * alone. A DEVICE fact, not a transport one — the same class of routing evidence as {@link hasP2p} —
242
+ * which is why it sits here rather than on {@link CommandContext}: it is as true of a described
243
+ * device as of a commanded one. The `rtsp` capability gates on it because a station serves an
244
+ * attached camera's stream itself and ignores that camera's authentication setting, so the write
245
+ * cannot do what its name promises there.
246
+ */
247
+ homeBaseAttached?: boolean;
200
248
  }
201
249
  export interface CommandContext extends AvailabilityContext {
202
250
  /** Device channel (0 for standalone, `device_channel` on a HomeBase). */
@@ -259,13 +307,6 @@ export interface CommandContext extends AvailabilityContext {
259
307
  * capability uses this to route lock/unlock to P2P vs. reject with a clear MQTT-not-wired error.
260
308
  */
261
309
  hasP2p?: boolean;
262
- /**
263
- * Whether the device hangs off a HomeBase (a `parent_sn` other than its own) rather than standing
264
- * alone. A DEVICE fact, not a transport one — the same class of routing evidence as {@link hasP2p}.
265
- * The `rtsp` capability gates on it because a station serves an attached camera's stream itself and
266
- * ignores that camera's authentication setting, so the write cannot do what its name promises there.
267
- */
268
- homeBaseAttached?: boolean;
269
310
  /**
270
311
  * Parsed `get_product_data_point` catalog for this device's SKU — present for vacuum/mower devices,
271
312
  * absent for all other codecs. Capabilities use it for per-model feature-availability and value-range
@@ -379,6 +379,23 @@ export type CarpetStrategy = (typeof CARPET_STRATEGIES)[number];
379
379
  */
380
380
  export declare const CLEAN_EXTENTS: readonly ["normal", "narrow", "quick"];
381
381
  export type CleanExtent = (typeof CLEAN_EXTENTS)[number];
382
+ /**
383
+ * Build a `CleanParamRequest` (DP 154) stating the three cleaning settings a run uses.
384
+ *
385
+ * The message is `{ clean_param: CleanParam }` — field 1 of the request, carrying the same `CleanParam`
386
+ * this module decodes out of field 1 of the reports it receives, so the shape a write sends is the
387
+ * shape a read has already been proven against on live hardware.
388
+ *
389
+ * All three settings are stated together because one message carries all three: a write naming fewer
390
+ * would be a `CleanParam` with the rest silent, and what a robot does with a half-stated one is not
391
+ * something this SDK has observed. `clean_times`(7) is never written — the vendor's own field comment
392
+ * makes zero mean "not stated", so omitting it leaves the robot's configured pass count alone — and
393
+ * neither is `fan`(6), which belongs to the suction capability's own data point.
394
+ *
395
+ * {@link VACUUM_CLEAN_MEMBERS.setCleanParam} dispatches this.
396
+ * @internal
397
+ */
398
+ export declare function encodeCleanParam(cleanType: VacuumCleanType, cleanExtent: CleanExtent, mopLevel: MopLevel): string;
382
399
  /**
383
400
  * Read one setting out of the CONFIGURED `CleanParam` (DP 154), by its field number.
384
401
  *
@@ -1963,6 +1980,48 @@ export declare const VACUUM_CLEAN_MEMBERS: {
1963
1980
  readonly startScene: import("./members.js").MethodMember<(sceneId: number) => Promise<void>> & {
1964
1981
  available: (ctx: import("./types.js").CommandContext) => boolean;
1965
1982
  };
1983
+ /**
1984
+ * State the cleaning settings a run uses — `CleanParamRequest.clean_param` over DP 154.
1985
+ *
1986
+ * The write counterpart of {@link VACUUM_CLEAN_MEMBERS.cleanType},
1987
+ * {@link VACUUM_CLEAN_MEMBERS.cleanExtent} and {@link VACUUM_CLEAN_MEMBERS.mopLevel}: one message
1988
+ * carries all three, so they are set together rather than through three setters that would each send
1989
+ * the same message with the other two silent.
1990
+ *
1991
+ * **The evidence, and its limit.** The frame is field 1 of `CleanParamRequest`, which carries the
1992
+ * very `CleanParam` this module decodes out of field 1 of the reports a live T2351 sends — the
1993
+ * field numbers, the single-field wrappers and the `mop_mode.level` scale are all read off that
1994
+ * capture, and `encodeCleanParam` writes what `decodeCleanParamValue` reads. What is NOT captured is
1995
+ * the write direction itself. It ships as a method rather than an unverified write because the
1996
+ * hazard that rule answers does not arise here: DP 154 is the robot's own settings report, so a frame
1997
+ * it does not accept leaves those three reads unchanged, where a wrong fire-and-forget command would
1998
+ * look exactly like success.
1999
+ *
2000
+ * Suction is not here. It has its own data point and its own capability — a `fan` field exists in
2001
+ * this message and is deliberately not written, for the same reason it is not read.
2002
+ */
2003
+ readonly setCleanParam: {
2004
+ readonly method: (deps: import("./members.js").MemberDeps) => (cleanType: VacuumCleanType, cleanExtent: CleanExtent, mopLevel: MopLevel) => Promise<void>;
2005
+ readonly description: string;
2006
+ readonly available: ((ctx: import("./types.js").CommandContext) => boolean) & ((ctx: import("./types.js").CommandContext) => boolean);
2007
+ readonly answers?: true;
2008
+ readonly args: readonly [{
2009
+ readonly name: "cleanType";
2010
+ readonly kind: "enum";
2011
+ readonly values: readonly ["sweep", "mop", "sweepAndMop", "sweepThenMop"];
2012
+ readonly description: "What the robot does with a surface.";
2013
+ }, {
2014
+ readonly name: "cleanExtent";
2015
+ readonly kind: "enum";
2016
+ readonly values: readonly ["normal", "narrow", "quick"];
2017
+ readonly description: "How far past the mapped edge a job reaches. Wire order, not app order.";
2018
+ }, {
2019
+ readonly name: "mopLevel";
2020
+ readonly kind: "enum";
2021
+ readonly values: readonly ["low", "middle", "high"];
2022
+ readonly description: "How much water the mop lays down. Only meaningful for a clean type that mops.";
2023
+ }];
2024
+ };
1966
2025
  /**
1967
2026
  * Clean the named rooms of a named map (ModeCtrlRequest method 1 over DP 152).
1968
2027
  *
@@ -71,9 +71,34 @@ export declare class MegaApiError extends Error {
71
71
  * device-list params carry the same `{param_type, param_value, update_time}` and are not owner-gated.
72
72
  */
73
73
  export declare const OWNER_ONLY_CODE = 20004;
74
- /** Thrown when a persisted/expired session is rejected (401). Re-login to recover. */
74
+ /**
75
+ * Thrown when a persisted/expired session is rejected (401). Re-login to recover.
76
+ *
77
+ * It carries the rate the client has already worked out for replacing a rejected token, because the
78
+ * rejection is where that rate stops being the client's alone: a login driven from here spends the same
79
+ * session the client's own recovery would have, and repeated logins are what makes an account start
80
+ * demanding captchas. {@link retryAfterMs} is how long the next replacement is barred for, and
81
+ * {@link contended} whether this rejection landed inside that bar — the shape repeated displacement has.
82
+ */
75
83
  export declare class SessionExpiredError extends Error {
76
- constructor(message: string);
84
+ /**
85
+ * How long the next session replacement is barred for, in milliseconds; `0` when nothing bars one now.
86
+ *
87
+ * The remainder of the client's own hold-off, which doubles per consecutive replacement and is capped —
88
+ * and which every replacement extends, whether the client spent it or a login made on this error did.
89
+ */
90
+ readonly retryAfterMs: number;
91
+ /**
92
+ * Whether this rejection landed inside that bar — a token replaced recently and rejected again since.
93
+ *
94
+ * It says the session is being DISPLACED rather than expiring: something else is signing in on this
95
+ * account, and replacing the token again only trades one login for another.
96
+ */
97
+ readonly contended: boolean;
98
+ constructor(message: string, opts?: {
99
+ retryAfterMs?: number;
100
+ contended?: boolean;
101
+ });
77
102
  }
78
103
  /**
79
104
  * eufy cloud gateway error codes — the numeric `code` carried in a response envelope alongside the
@@ -223,9 +248,11 @@ export declare class MegaHttpClient {
223
248
  private loggingIn;
224
249
  /** The one in-flight re-login every call rejected on the same dead token waits on. */
225
250
  private reauthAttempt?;
226
- /** Replacements since the held session last proved stable, and when the last one ran — see {@link recoveryDue}. */
251
+ /** Replacements since the held session last proved stable, and when the last one ran — see {@link holdOffRemainingMs}. */
227
252
  private recoveries;
228
253
  private lastRecoveryAt;
254
+ /** A token of ours has been rejected and not yet replaced — see {@link noteTokenReplacement}. */
255
+ private rejectedTokenPending;
229
256
  constructor(cfg: MegaClientConfig);
230
257
  /**
231
258
  * Install the session the store holds, if it holds a usable one: the token + its bound ECDH key, skipping
@@ -500,6 +527,26 @@ export declare class MegaHttpClient {
500
527
  * and a caller is told the honest reason instead of being served a fight.
501
528
  */
502
529
  private recoveryDue;
530
+ /**
531
+ * How much longer a token replacement must wait, in milliseconds; `0` when one may run now.
532
+ *
533
+ * The wait doubles per consecutive replacement and is capped, and it is what {@link recoveryDue} gates
534
+ * this client's own recovery on — and what {@link SessionExpiredError.retryAfterMs} hands a host that
535
+ * drives its own. One function so the two cannot disagree about the rate, which they would have to for
536
+ * a host to be told it may retry while this client is still holding off.
537
+ */
538
+ private holdOffRemainingMs;
539
+ /**
540
+ * Count one token replacement against the hold-off, and clear the rejection it answered.
541
+ *
542
+ * Every replacement passes through here, wherever it was spent from: {@link recoverRejectedSession}, and
543
+ * a {@link login} that follows a rejection this client surfaced. A hold-off that counted only its own
544
+ * would be no bound at all — the wait would sit at its first value however many sessions had been spent,
545
+ * and {@link SessionExpiredError.retryAfterMs} would report a minute while logins ran every few seconds.
546
+ * Which of the two counted a given replacement is the flag: the recovery path clears it before logging
547
+ * in, so the login cannot count the same one again.
548
+ */
549
+ private noteTokenReplacement;
503
550
  /**
504
551
  * Note that the held session is working. A replacement that keeps serving calls for long enough is not
505
552
  * contention, so the hold-off is forgotten and the next genuine expiry recovers immediately.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mega-yfue/eufy-sdk",
3
- "version": "0.2.0-beta.20",
3
+ "version": "0.2.0-beta.22",
4
4
  "description": "One typed TypeScript client for the Anker eufy v6 cloud — capability-driven devices, realtime events over P2P/MQTT/push, and live media",
5
5
  "license": "Apache-2.0",
6
6
  "author": "mega-yfue",