@mega-yfue/eufy-sdk 0.2.0-beta.0 → 0.2.0-beta.2

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.
@@ -2,11 +2,15 @@ import { type Surface } from "./members.js";
2
2
  import type { CapabilityModule, CommandContext } from "./types.js";
3
3
  import type { Command } from "../../core/contracts.js";
4
4
  /**
5
- * The guard modes {@link ArmingActions} can SET — the three whose write was captured byte-exact against a
6
- * real station. `ArmingMode` is both the const value-object (`ArmingMode.home`) and the union type of its
5
+ * The guard modes `setMode` can SET — the four whose write is confirmed against a real station. Three are
6
+ * byte-exact captures and `custom1` is a live confirmation; `ARMING_MODE_WIRE` carries the per-value
7
+ * evidence. `ArmingMode` is both the const value-object (`ArmingMode.home`) and the union type of its
7
8
  * values, so callers pass the named constant: `setMode(ArmingMode.home)`.
8
9
  *
9
- * Deliberately NARROWER than the set a device may report. The other six modes are ones the app itself
10
+ * The domain of `setMode` (cmd 1224) alone. The alarm-delay write (cmd 1255) carries its own mode integer
11
+ * on a separate wire and takes {@link AlarmDelayMode}.
12
+ *
13
+ * Deliberately NARROWER than the set a device may report. The remaining five modes are ones the app itself
10
14
  * defines and the `mode` read still names them, but no capture shows one being SENT — and
11
15
  * on a fire-and-forget wire a wrong one looks exactly like success. Leaving them out of this union is the
12
16
  * compile-time half of the refusal; `mode`'s published argument and the generated rejection are the
@@ -17,10 +21,31 @@ export declare const ArmingMode: {
17
21
  readonly away: "away";
18
22
  /** Armed for occupancy — reduced/perimeter protection while home (wire value 1). */
19
23
  readonly home: "home";
24
+ /** Custom 1 — a user-defined posture configured in the app (wire value 3). */
25
+ readonly custom1: "custom1";
20
26
  /** Disarmed — no alarms; sensors still report state (wire value 63). */
21
27
  readonly disarmed: "disarmed";
22
28
  };
23
29
  export type ArmingMode = (typeof ArmingMode)[keyof typeof ArmingMode];
30
+ /**
31
+ * The modes the alarm-delay write (cmd 1255) accepts a `mode_id` for — the three guard modes whose wire
32
+ * integer is byte-captured.
33
+ *
34
+ * A domain of its own, because the two mode integers ride different commands: `setMode` writes `mode_type`
35
+ * on cmd 1224, and cmd 1255 carries `mode_id`. Neither wire validates the integer, so each union IS its
36
+ * command's gate, and evidence for one is not evidence for the other. `custom1` is confirmed on 1224 only;
37
+ * 1255 has no capture carrying mode 3, and no known GET to read one back — the app's own replies
38
+ * `{count:0,data:null}` — so it is absent here. Narrower than {@link ArmingMode} by exactly that value.
39
+ */
40
+ export declare const AlarmDelayMode: {
41
+ /** Armed — full protection, nobody home (wire value 0). */
42
+ readonly away: "away";
43
+ /** Armed for occupancy — reduced/perimeter protection while home (wire value 1). */
44
+ readonly home: "home";
45
+ /** Disarmed — no alarms; sensors still report state (wire value 63). */
46
+ readonly disarmed: "disarmed";
47
+ };
48
+ export type AlarmDelayMode = (typeof AlarmDelayMode)[keyof typeof AlarmDelayMode];
24
49
  /**
25
50
  * The P2P **feature-command ids** this arming capability drives. Capability-owned wire vocabulary
26
51
  * (transport forwards `cmd.param` opaquely; full id→name catalog in the generated
@@ -34,10 +59,11 @@ export declare const ARMING_CMD: {
34
59
  * mValue3:0, `payload:{mode_type:<int>, user_name:<string>}`.
35
60
  *
36
61
  * ⚠️ Only 3 of the 9 modes were exercised in that capture — `mode_type` 0 (away), 63 (disarmed), 1
37
- * (home), all confirmed byte-exact, and those three are the whole of {@link ArmingMode}. Re-confirmed
38
- * live 2026-08-05: each reported its own MODE_SWITCH push within ~5s of the write. The remaining six are
39
- * named by the app but never observed leaving it, so this capability reads them and refuses to send
40
- * them. See `ARMING_MODE_WIRE` for the per-value breakdown.
62
+ * (home), all confirmed byte-exact. Re-confirmed live 2026-08-05: each reported its own MODE_SWITCH push
63
+ * within ~5s of the write. `custom1` 3 joined {@link ArmingMode} on a live confirmation rather than a
64
+ * capture, making four settable in total. The remaining five are named by the app but never observed
65
+ * leaving it, so this capability reads them and refuses to send them. See `ARMING_MODE_WIRE` for the
66
+ * per-value breakdown.
41
67
  */
42
68
  readonly SET_ARMING: 1224;
43
69
  /**
@@ -139,9 +165,9 @@ export type ArmingActions = Surface<typeof ARMING_MEMBERS>;
139
165
  export declare const ARMING_MEMBERS: {
140
166
  /**
141
167
  * The one member whose write domain is NARROWER than its read: `enumValues` names all nine modes a
142
- * station can report, and the argument's `values` publishes only the three whose wire was captured. That
143
- * argument IS the domain the derived setter enforces and the refusal names, so an uncaptured mode is
144
- * refused by naming the three that work — nine labels for the read and three for the write, off one
168
+ * station can report, and the argument's `values` publishes only the four whose write is confirmed. That
169
+ * argument IS the domain the derived setter enforces and the refusal names, so an unconfirmed mode is
170
+ * refused by naming the four that work — nine labels for the read and four for the write, off one
145
171
  * declaration.
146
172
  *
147
173
  * `armingCommand` may also throw synchronously (missing account identity) and `bindMembers` turns that
@@ -187,15 +213,17 @@ export declare const ARMING_MEMBERS: {
187
213
  * this mode — there is no known GET to fetch it automatically, and a wrong guess here can silently
188
214
  * misconfigure which sensors arm/trigger for real.
189
215
  *
190
- * Takes {@link ArmingMode}, so a delay can only be configured for a mode whose `mode_id` integer is
191
- * captured. The frame carries that same integer, so a schedule/custom mode would be the identical guess
192
- * `setMode` refuses.
216
+ * Takes {@link AlarmDelayMode}, not {@link ArmingMode}: a delay is configurable only for a mode whose
217
+ * integer is captured on THIS command, and `custom1` is confirmed on cmd 1224 only. The frame carries
218
+ * that integer in `mode_id` with no runtime validation and no readback, so a mode outside this union
219
+ * would be the same unverified guess `setMode` refuses.
193
220
  */
194
- readonly setAlarmDelayConfig: import("./members.js").MethodMember<(mode: ArmingMode, config: AlarmDelayConfig) => Promise<void>>;
221
+ readonly setAlarmDelayConfig: import("./members.js").MethodMember<(mode: AlarmDelayMode, config: AlarmDelayConfig) => Promise<void>>;
195
222
  };
196
223
  /**
197
224
  * `arming` — guard/arming mode. `armingMode` (see {@link ARMING_CMD.SET_ARMING}) has a verified
198
- * read/write MECHANISM, but only 3 of its 8 {@link ArmingMode} values (away/home/disarmed) are
199
- * wire-captured — see `ARMING_MODE_WIRE` for which 5 are still unverified third-party integers.
225
+ * read/write MECHANISM, but only 4 of the 9 modes it reports (away/home/custom1/disarmed, the
226
+ * {@link ArmingMode} union) are confirmed as writes — see `ARMING_MODE_WIRE` for which 5 are still
227
+ * unverified third-party integers, and which of the 4 is live-confirmed rather than byte-captured.
200
228
  */
201
229
  export declare const ARMING: CapabilityModule;
@@ -385,7 +385,7 @@ export interface DeviceActionMap {
385
385
  lock: LockActions;
386
386
  /** Siren: reads `active`, `volume`, `alarmDuration`, `doNotDisturb`; writes `setVolume`, `setAlarmDuration`, `test`, `stop` (config setters present when the param is reported). No direct "sound the alarm" wire — a real alarm is driven by the `arming` system; `test` is the on-demand trigger. */
387
387
  siren: SirenActions;
388
- /** Guard mode: `setMode(ArmingMode)` + `setAlarmDelayConfig(mode, config)`. Of the 8 `ArmingMode` values only `away`/`home`/`disarmed` are confirmed on-device. */
388
+ /** Guard mode: `setMode(ArmingMode)` + `setAlarmDelayConfig(mode, config)`. Of the 9 modes a station reports, only `away`/`home`/`custom1`/`disarmed` are confirmed as writes (`ArmingMode`); the alarm-delay write takes the narrower byte-captured `AlarmDelayMode`. */
389
389
  arming: ArmingActions;
390
390
  /** Doorbell: `playQuickResponse(voiceId)` (the canned voice replies). */
391
391
  doorbell: DoorbellActions;
@@ -555,7 +555,7 @@ export type { VacuumActivity, VacuumCleanType, CarpetStrategy, CleanExtent } fro
555
555
  export { VACUUM_ACTIVITIES, VACUUM_CLEAN_TYPES, CARPET_STRATEGIES, CLEAN_EXTENTS, MOP_LEVELS } from "./vacuum-clean.js";
556
556
  export { SuctionLevel, suctionLevelName, type SuctionLevelValue } from "./suction.js";
557
557
  export type { PtzPresetActions, ZoomRegion, PtzPreset, PtzPresetImage } from "./ptz.js";
558
- export { ArmingMode } from "./arming.js";
558
+ export { AlarmDelayMode, ArmingMode } from "./arming.js";
559
559
  export type { AlarmDelayConfig, AlarmDelayCountdown, AlarmDelayDeviceAction, AlarmDelaySeconds } from "./arming.js";
560
560
  export { PtzDirection } from "./ptz.js";
561
561
  export { AiDetectType, encodeAiDetectType, decodeAiDetectType, type AiDetectFlags } from "./motion.js";
@@ -186,8 +186,16 @@ export declare class MegaHttpClient {
186
186
  * restored-session short-circuit must NOT treat it as a usable session. Cleared on Ok/reset. */
187
187
  private pending2fa;
188
188
  private tokenExpiresAt;
189
- /** Stable per-install device id — the auth token is bound to it. */
190
- private openudid;
189
+ /**
190
+ * Stable per-install device id: `openudid` as configured, as restored from the session store, or as
191
+ * derived from the ACCOUNT when neither supplied one — two clients that configure none therefore
192
+ * share it, and are one install as far as everything keyed on this is concerned.
193
+ *
194
+ * Two things are keyed on it, and both fail the same way when it is shared: the auth token is bound
195
+ * to it, so each login displaces the other's session, and the secure-MQTT client id is built from it,
196
+ * so each connection evicts the other's channel.
197
+ */
198
+ readonly openudid: string;
191
199
  /** The device model reported to the cloud (explicit `phoneModel`, else a stable random one). */
192
200
  private readonly phoneModel;
193
201
  /** The `user-agent` for the media-download path (explicit `mediaUserAgent`, else derived from the model). */
@@ -11,6 +11,12 @@ export interface AppClientIdInput {
11
11
  }
12
12
  /** Build a client_id shaped like `android-{appName}-{uid}-{mqttUuid}-{timestamp}`. */
13
13
  export declare function buildAppShapedClientId(input: AppClientIdInput): string;
14
- /** A fresh stable-looking install UUID (16 hex chars) — generate ONCE per identity and persist it
15
- * (a new random value on every connect defeats the point of "stable"). */
16
- export declare function generateMqttUuid(): string;
14
+ /**
15
+ * The `mqttUuid` segment for a client bound to `installId`, hashed to the 16-hex shape
16
+ * {@link buildAppShapedClientId} expects. Deterministic, so a client keeps its id across restarts and
17
+ * takes its own stale session over rather than doubling up beside it; one-way, so the id it is derived
18
+ * from is not recoverable from a client_id that travels the wire in clear.
19
+ *
20
+ * Two clients are distinguished exactly as far as their `installId` is: equal ids in, equal ids out.
21
+ */
22
+ export declare function mqttUuidFrom(installId: string): string;
@@ -87,9 +87,6 @@ export interface MqttRouterDeps {
87
87
  export declare class MqttCommandRouter {
88
88
  private readonly deps;
89
89
  private readonly logger;
90
- /** Stable per-process install id for the app-shaped MQTT client_id (see {@link buildAppShapedClientId}) —
91
- * generated once, reused for every security-MQTT connect this router makes. */
92
- private mqttUuid?;
93
90
  constructor(deps: MqttRouterDeps);
94
91
  /**
95
92
  * Whether this transport stack drives `dev`'s `ff09-*` commands — a **eufy-cloud device**
@@ -14,6 +14,16 @@
14
14
  import { EventEmitter } from "node:events";
15
15
  import type { EufyDevice, RealtimeTransport } from "../../core/types.js";
16
16
  import { type Logger } from "../../core/logger.js";
17
+ /**
18
+ * Whether a connect failed because the broker REFUSED the client — a CONNACK return code the client
19
+ * cannot retry its way out of, as `mqtt.js` words it (`Connection refused: not authorized`). A socket
20
+ * that dies without an answer is not this: it is the same request, unanswered, and retrying it is the
21
+ * only way to learn which of the two happened.
22
+ *
23
+ * A refusal that arrives as a dropped connection instead of a CONNACK reads here as the transport
24
+ * failure it is indistinguishable from.
25
+ */
26
+ export declare function isNotAuthorized(err: unknown): boolean;
17
27
  /**
18
28
  * Per-user mTLS credentials as returned by get_user_mqtt_info.
19
29
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mega-yfue/eufy-sdk",
3
- "version": "0.2.0-beta.0",
3
+ "version": "0.2.0-beta.2",
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",