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

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.
Files changed (38) hide show
  1. package/dist/client/eufy-mega.d.ts +16 -11
  2. package/dist/core/contracts.d.ts +57 -27
  3. package/dist/core/crypto.d.ts +10 -0
  4. package/dist/core/index.d.ts +1 -0
  5. package/dist/core/logger.d.ts +5 -3
  6. package/dist/core/solix-types.d.ts +36 -0
  7. package/dist/core/store.d.ts +20 -9
  8. package/dist/index.js +1446 -192
  9. package/dist/index.js.map +4 -4
  10. package/dist/model/capabilities/arming.d.ts +58 -28
  11. package/dist/model/capabilities/display.d.ts +85 -0
  12. package/dist/model/capabilities/index.d.ts +11 -5
  13. package/dist/model/capabilities/solix.d.ts +75 -0
  14. package/dist/model/capabilities/types.d.ts +16 -4
  15. package/dist/model/capabilities/vacuum-clean.d.ts +74 -25
  16. package/dist/model/index.d.ts +3 -0
  17. package/dist/model/param-dictionary.d.ts +24 -0
  18. package/dist/model/param-namespace.d.ts +1 -1
  19. package/dist/model/solix-catalog.d.ts +20 -0
  20. package/dist/model/solix-device.d.ts +102 -0
  21. package/dist/model/types.d.ts +5 -5
  22. package/dist/transport/ff09.d.ts +7 -0
  23. package/dist/transport/http/index.d.ts +1 -0
  24. package/dist/transport/http/solix-client.d.ts +158 -0
  25. package/dist/transport/http/solix-constants.d.ts +29 -0
  26. package/dist/transport/mqtt/index.d.ts +2 -0
  27. package/dist/transport/mqtt/secure-mqtt.d.ts +14 -1
  28. package/dist/transport/mqtt/solix-mqtt.d.ts +214 -0
  29. package/dist/transport/mqtt/topics.d.ts +20 -0
  30. package/dist/transport/p2p/command-router.d.ts +143 -15
  31. package/dist/transport/p2p/index.d.ts +1 -0
  32. package/dist/transport/p2p/live-stream.d.ts +5 -4
  33. package/dist/transport/p2p/live-trace.d.ts +100 -6
  34. package/dist/transport/p2p/media.d.ts +11 -0
  35. package/dist/transport/p2p/p2p-session.d.ts +4 -0
  36. package/dist/transport/p2p/session-manager.d.ts +57 -23
  37. package/dist/transport/p2p/shared-live-source.d.ts +10 -1
  38. package/package.json +3 -2
@@ -2,25 +2,58 @@ 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 — every mode a station reports, all nine confirmed against real
6
+ * hardware. `ArmingMode` is both the const value-object (`ArmingMode.home`) and the union type of its
7
7
  * values, so callers pass the named constant: `setMode(ArmingMode.home)`.
8
8
  *
9
- * Deliberately NARROWER than the set a device may report. The other six modes are ones the app itself
10
- * defines and the `mode` read still names them, but no capture shows one being SENT — and
11
- * on a fire-and-forget wire a wrong one looks exactly like success. Leaving them out of this union is the
12
- * compile-time half of the refusal; `mode`'s published argument and the generated rejection are the
13
- * runtime half.
9
+ * The domain of `setMode` (cmd 1224) alone. The alarm-delay write (cmd 1255) carries its own mode integer
10
+ * on a separate wire and takes {@link AlarmDelayMode}, which stays NARROWER — evidence for one command is
11
+ * not evidence for the other, and 1255 still has no capture beyond its byte-captured three.
12
+ *
13
+ * A mode belongs here only once its write is confirmed against real hardware, because a fire-and-forget
14
+ * wire makes a wrong mode look exactly like success; `ARMING_MODE_WIRE` carries the per-value evidence.
15
+ * The union is also what `ARMING_MODE_WIRE` is keyed by, so the table cannot name a mode this does not.
14
16
  */
15
17
  export declare const ArmingMode: {
16
18
  /** Armed — full protection, nobody home (wire value 0). */
17
19
  readonly away: "away";
18
20
  /** Armed for occupancy — reduced/perimeter protection while home (wire value 1). */
19
21
  readonly home: "home";
22
+ /** Scheduled — the station follows the timetable configured in the app (wire value 2). */
23
+ readonly schedule: "schedule";
24
+ /** Custom 1 — a user-defined posture configured in the app (wire value 3). */
25
+ readonly custom1: "custom1";
26
+ /** Custom 2 — a user-defined posture configured in the app (wire value 4). */
27
+ readonly custom2: "custom2";
28
+ /** Custom 3 — a user-defined posture configured in the app (wire value 5). */
29
+ readonly custom3: "custom3";
30
+ /** Off — the station's alarm system is switched off entirely (wire value 6). */
31
+ readonly off: "off";
32
+ /** Geofenced — the station follows the app's location-based rules (wire value 47). */
33
+ readonly geo: "geo";
20
34
  /** Disarmed — no alarms; sensors still report state (wire value 63). */
21
35
  readonly disarmed: "disarmed";
22
36
  };
23
37
  export type ArmingMode = (typeof ArmingMode)[keyof typeof ArmingMode];
38
+ /**
39
+ * The modes the alarm-delay write (cmd 1255) accepts a `mode_id` for — the three guard modes whose wire
40
+ * integer is byte-captured.
41
+ *
42
+ * A domain of its own, because the two mode integers ride different commands: `setMode` writes `mode_type`
43
+ * on cmd 1224, and cmd 1255 carries `mode_id`. Neither wire validates the integer, so each union IS its
44
+ * command's gate, and evidence for one is not evidence for the other. `custom1` is confirmed on 1224 only;
45
+ * 1255 has no capture carrying mode 3, and no known GET to read one back — the app's own replies
46
+ * `{count:0,data:null}` — so it is absent here. Narrower than {@link ArmingMode} by exactly that value.
47
+ */
48
+ export declare const AlarmDelayMode: {
49
+ /** Armed — full protection, nobody home (wire value 0). */
50
+ readonly away: "away";
51
+ /** Armed for occupancy — reduced/perimeter protection while home (wire value 1). */
52
+ readonly home: "home";
53
+ /** Disarmed — no alarms; sensors still report state (wire value 63). */
54
+ readonly disarmed: "disarmed";
55
+ };
56
+ export type AlarmDelayMode = (typeof AlarmDelayMode)[keyof typeof AlarmDelayMode];
24
57
  /**
25
58
  * The P2P **feature-command ids** this arming capability drives. Capability-owned wire vocabulary
26
59
  * (transport forwards `cmd.param` opaquely; full id→name catalog in the generated
@@ -34,10 +67,11 @@ export declare const ARMING_CMD: {
34
67
  * mValue3:0, `payload:{mode_type:<int>, user_name:<string>}`.
35
68
  *
36
69
  * ⚠️ 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.
70
+ * (home), all confirmed byte-exact. Re-confirmed live 2026-08-05: each reported its own MODE_SWITCH push
71
+ * within ~5s of the write. The remaining six are live confirmations rather than captures — `custom1` 3
72
+ * first, then `schedule` 2, `custom2` 4, `custom3` 5, `off` 6 and `geo` 47 — each sent as this exact
73
+ * frame and each observed to bring MODE_SWITCH back, so all nine are settable. `ARMING_MODE_WIRE` has
74
+ * the per-value evidence and the dates.
41
75
  */
42
76
  readonly SET_ARMING: 1224;
43
77
  /**
@@ -138,11 +172,10 @@ export type ArmingActions = Surface<typeof ARMING_MEMBERS>;
138
172
  */
139
173
  export declare const ARMING_MEMBERS: {
140
174
  /**
141
- * 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
145
- * declaration.
175
+ * Read and write are the same nine modes, so `enumValues` is the whole domain: `writeDomain` falls back
176
+ * to it, and the derived setter, the refusal message and the offered argument all read from that one
177
+ * declaration. A member states an `args` entry only where the two sides DIFFER. See
178
+ * {@link ARMING_MODE_WIRE} for the per-value evidence.
146
179
  *
147
180
  * `armingCommand` may also throw synchronously (missing account identity) and `bindMembers` turns that
148
181
  * into a rejection, so the builder stays plain.
@@ -162,11 +195,6 @@ export declare const ARMING_MEMBERS: {
162
195
  readonly kind: "enum";
163
196
  readonly enumValues: Record<number, string>;
164
197
  readonly provenance: "verified";
165
- readonly args: readonly [{
166
- readonly name: "mode";
167
- readonly kind: "enum";
168
- readonly values: readonly number[];
169
- }];
170
198
  readonly description: string;
171
199
  readonly observation: {
172
200
  readonly event: "armingModeChanged";
@@ -187,15 +215,17 @@ export declare const ARMING_MEMBERS: {
187
215
  * this mode — there is no known GET to fetch it automatically, and a wrong guess here can silently
188
216
  * misconfigure which sensors arm/trigger for real.
189
217
  *
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.
218
+ * Takes {@link AlarmDelayMode}, not {@link ArmingMode}: a delay is configurable only for a mode whose
219
+ * integer is captured on THIS command, and `custom1` is confirmed on cmd 1224 only. The frame carries
220
+ * that integer in `mode_id` with no runtime validation and no readback, so a mode outside this union
221
+ * would be the same unverified guess `setMode` refuses.
193
222
  */
194
- readonly setAlarmDelayConfig: import("./members.js").MethodMember<(mode: ArmingMode, config: AlarmDelayConfig) => Promise<void>>;
223
+ readonly setAlarmDelayConfig: import("./members.js").MethodMember<(mode: AlarmDelayMode, config: AlarmDelayConfig) => Promise<void>>;
195
224
  };
196
225
  /**
197
- * `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.
226
+ * `arming` — guard/arming mode. `armingMode` (see {@link ARMING_CMD.SET_ARMING}) has a verified read/write
227
+ * MECHANISM, and all 9 modes it reports are now confirmed as writes — the {@link ArmingMode} union is the
228
+ * whole set. See `ARMING_MODE_WIRE` for which three are byte-captured and which six are live-confirmed.
229
+ * The alarm-delay write (cmd 1255) is unaffected and keeps its narrower {@link AlarmDelayMode}.
200
230
  */
201
231
  export declare const ARMING: CapabilityModule;
@@ -0,0 +1,85 @@
1
+ import type { CapabilityModule } from "./types.js";
2
+ import { type Surface } from "./members.js";
3
+ /**
4
+ * `display` — a eufy Smart Display's charge.
5
+ *
6
+ * One typed read, and the smallness is the finding rather than a gap. The captured unit (a T87A0,
7
+ * 2026-09-04) connects over secure MQTT with no `p2p_did`, so it speaks no P2P at all, and it reported
8
+ * six params in an id range no other line uses. Only one of them tells a caller something it could not
9
+ * get another way:
10
+ *
11
+ * - 8005 and 8006 restate the model's retail name and code, which `info` already answers from the
12
+ * curated registry and the cloud record. Their evidence IS that agreement, so a getter beside `info`
13
+ * would offer a caller a second spelling of what it just read. They are named in the display param
14
+ * dictionary, which is what makes them readable off `getProperties()`.
15
+ * - 8003 is version-shaped and nothing confirms what it means, so it is `unexposed`: in the schema with
16
+ * its type and its `guessed` label, reachable through `getProperty`, no typed getter.
17
+ *
18
+ * Nothing is writable. No capture pins a write for any display param, and an AIoT write is
19
+ * fire-and-forget, so a wrong frame to a device that acknowledges nothing looks exactly like success. A
20
+ * screen, a volume, an assistant: the device plainly has all three and reports none of them in anything
21
+ * captured, which means the reads have to arrive before a control can be honest about what it moves.
22
+ *
23
+ * @module model/capabilities/display
24
+ */
25
+ /** Smart Display param ids this capability reads. The rest of the range is named in the dictionary. */
26
+ export declare const DISPLAY_PARAM: {
27
+ readonly BATTERY: 8001;
28
+ readonly SOFTWARE_VERSION: 8003;
29
+ };
30
+ /**
31
+ * The `display` reads. Read-only; see the module note for why there is no write.
32
+ *
33
+ * Exported but NOT published: the entries state their wire ids and what each claim rests on, which the
34
+ * reference site does not carry.
35
+ * @internal
36
+ */
37
+ export declare const DISPLAY_MEMBERS: {
38
+ /**
39
+ * Battery level, 0-100.
40
+ *
41
+ * **On this capability rather than on `battery`, and that is the line partition doing its job.** The
42
+ * security-line `battery` capability reads param 1101 and a Smart Display's charge is 8001 in its own
43
+ * id space — two wires that happen to mean the same thing. One capability reading both would be a claim
44
+ * that the two ecosystems share a param space, so a consumer reads a display's charge through
45
+ * `dev.display()` and a camera's through `dev.battery()`.
46
+ *
47
+ * `verified` rather than `mega`: the id is in the device's cloud record, but the NAME came from the
48
+ * maintainer's own knowledge of the hardware rather than from the cloud data-point list, and `"100"`
49
+ * fits brightness, volume or charge equally.
50
+ *
51
+ * The scale is `percent` on the reading itself, not on convention alone: a full charge reads `255` on a
52
+ * 0-255 scale and `1000` on a 0-1000 one, so `"100"` on a charged unit is positive evidence for 0-100
53
+ * rather than merely consistent with it. What nobody has done is watch it MOVE, which is why a value
54
+ * frozen at 100 would not yet be distinguishable from a healthy one.
55
+ */
56
+ readonly battery: {
57
+ readonly param: 8001;
58
+ readonly type: "number";
59
+ readonly unit: "%";
60
+ readonly kind: "percent";
61
+ readonly provenance: "verified";
62
+ readonly description: "Battery level, 0-100 (param 8001).";
63
+ };
64
+ /**
65
+ * Version-shaped, and that shape is the whole of the evidence — hence `guessed`, and hence no typed
66
+ * getter: a caller reading this off a bound object cannot see the label.
67
+ *
68
+ * `unexposed` rather than absent, so the schema still carries its type and that label and
69
+ * `getProperty("softwareVersion")` still answers. `dev.info()?.firmwareVersion` is the field to trust
70
+ * where the cloud record carries one; on this display it does not, which is the only reason 8003 is
71
+ * named at all.
72
+ */
73
+ readonly softwareVersion: {
74
+ readonly param: 8003;
75
+ readonly type: "string";
76
+ readonly kind: "text";
77
+ readonly provenance: "guessed";
78
+ readonly unexposed: true;
79
+ readonly description: "Version-shaped string (param 8003), meaning unconfirmed — prefer `info.firmwareVersion`.";
80
+ };
81
+ };
82
+ /** `display` — a eufy Smart Display (T87Ax). Read-only; no display write is captured. */
83
+ export declare const DISPLAY: CapabilityModule;
84
+ /** Bound Smart Display reads — the object returned by `dev.display()`. Read-only. */
85
+ export type DisplayActions = Surface<typeof DISPLAY_MEMBERS>;
@@ -40,6 +40,7 @@ import type { KeypadActions } from "./keypad.js";
40
40
  import type { RtspActions } from "./rtsp.js";
41
41
  import type { VacuumCleanActions } from "./vacuum-clean.js";
42
42
  import type { VacuumDockActions } from "./vacuum-dock.js";
43
+ import type { DisplayActions } from "./display.js";
43
44
  import type { SuctionActions } from "./suction.js";
44
45
  import type { LocateActions } from "./locate.js";
45
46
  import type { DeviceInfo } from "./info.js";
@@ -74,10 +75,12 @@ export declare function mergeProperties(caps: Capability[], ctx?: AvailabilityCo
74
75
  * - a `modelHints` regex matches the model/category/name haystack,
75
76
  * - `codecs` includes `codec`,
76
77
  * - `detect(rec, codec)` returns true.
77
- * Never throws. Returns a de-duplicated array.
78
+ *
79
+ * An absent `codec` belongs to no line, so only the line-agnostic capabilities can match — the truthful
80
+ * answer for a device outside the eufy families entirely. Never throws. Returns a de-duplicated array.
78
81
  * @internal
79
82
  */
80
- export declare function detectCapabilities(rec: CloudRecord, codec: Codec): Capability[];
83
+ export declare function detectCapabilities(rec: CloudRecord, codec?: Codec): Capability[];
81
84
  /**
82
85
  * The baseline capabilities a codec grants every device of that family — derived from the modules
83
86
  * that declare the codec in their {@link import("./types").DetectionSpec} `codecs`. Each capability
@@ -385,7 +388,7 @@ export interface DeviceActionMap {
385
388
  lock: LockActions;
386
389
  /** 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
390
  siren: SirenActions;
388
- /** Guard mode: `setMode(ArmingMode)` + `setAlarmDelayConfig(mode, config)`. Of the 8 `ArmingMode` values only `away`/`home`/`disarmed` are confirmed on-device. */
391
+ /** 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
392
  arming: ArmingActions;
390
393
  /** Doorbell: `playQuickResponse(voiceId)` (the canned voice replies). */
391
394
  doorbell: DoorbellActions;
@@ -411,6 +414,8 @@ export interface DeviceActionMap {
411
414
  suction: SuctionActions;
412
415
  /** RoboVac locate (find-robot beep): `locating`; `locate(on?)`. */
413
416
  locate: LocateActions;
417
+ /** Smart Display (read-only): `battery`. No display write is captured. */
418
+ display: DisplayActions;
414
419
  /** Identity metadata (read-only): `{ manufacturer, model, serialNumber, name, deviceType?, firmwareVersion?, hardwareVersion? }`. */
415
420
  info: DeviceInfo;
416
421
  }
@@ -529,6 +534,7 @@ export { RTSP_MEMBERS } from "./rtsp.js";
529
534
  export { SIREN_MEMBERS } from "./siren.js";
530
535
  export { SMART_LIGHT_MEMBERS } from "./smart-light.js";
531
536
  export { SMOKE_MEMBERS } from "./smoke.js";
537
+ export { DISPLAY_MEMBERS, type DisplayActions } from "./display.js";
532
538
  export { SUCTION_MEMBERS } from "./suction.js";
533
539
  export { VACUUM_CLEAN_MEMBERS } from "./vacuum-clean.js";
534
540
  export type { DeviceInfo } from "./info.js";
@@ -550,12 +556,12 @@ export { HubAlarmTone, type HubAlarmToneValue } from "./siren.js";
550
556
  * RoboVac activity and clean type are the declared returns of the public `dev.vacuumClean()` getters,
551
557
  * so both unions are published.
552
558
  */
553
- export type { VacuumActivity, VacuumCleanType, CarpetStrategy, CleanExtent } from "./vacuum-clean.js";
559
+ export type { VacuumActivity, VacuumCleanType, CarpetStrategy, CleanExtent, VacuumRoomTarget, VacuumZoneTarget, } from "./vacuum-clean.js";
554
560
  /** The lists those unions are taken from — published because each union names its own. */
555
561
  export { VACUUM_ACTIVITIES, VACUUM_CLEAN_TYPES, CARPET_STRATEGIES, CLEAN_EXTENTS, MOP_LEVELS } from "./vacuum-clean.js";
556
562
  export { SuctionLevel, suctionLevelName, type SuctionLevelValue } from "./suction.js";
557
563
  export type { PtzPresetActions, ZoomRegion, PtzPreset, PtzPresetImage } from "./ptz.js";
558
- export { ArmingMode } from "./arming.js";
564
+ export { AlarmDelayMode, ArmingMode } from "./arming.js";
559
565
  export type { AlarmDelayConfig, AlarmDelayCountdown, AlarmDelayDeviceAction, AlarmDelaySeconds } from "./arming.js";
560
566
  export { PtzDirection } from "./ptz.js";
561
567
  export { AiDetectType, encodeAiDetectType, decodeAiDetectType, type AiDetectFlags } from "./motion.js";
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Anker **Solix** capability surface. Solix is a separate ecosystem — its own Anker account, backend
3
+ * and product catalog — so it keeps its own capability id union rather than joining eufy's `Capability`
4
+ * / `Codec` unions, and detection is by Anker catalog CATEGORY + product-code prefix (see
5
+ * {@link detectSolixCapabilities}) rather than eufy param ids.
6
+ *
7
+ * What it shares is the `members` engine: the one feature with a readable wire declares ONE `members`
8
+ * table, and its property schema, evidence gate and typed surface all derive from it through
9
+ * `members.ts` (`bindMembers` / `Surface`), exactly as a eufy capability does.
10
+ *
11
+ * @module model/capabilities/solix
12
+ */
13
+ import type { Surface } from "./members.js";
14
+ /** Every capability a Solix device may carry. Solix's OWN union (not eufy's `Capability`). */
15
+ export type SolixCapability = "identity" | "firmware" | "connectivity" | "energyMeter" | "battery" | "solarInput" | "acOutput" | "evCharger" | "charger" | "cooler";
16
+ /**
17
+ * The `energyMeter` surface, declared once. Only the ONE confirmed tag→name binding is a member:
18
+ * `meterVoltageL1` (ff09 tag `0xAC`), confirmed against a live single-phase frame. The evidence gate
19
+ * (`bindMembers`) installs its getter only once a frame carrying tag `0xAC` has landed and answers
20
+ * `undefined` before.
21
+ *
22
+ * The meter reports many more quantities (per-line power/current/voltage, totals, import/export energy),
23
+ * but their tag→name bindings are a structural inference not yet pinned to a known-load capture. Rather
24
+ * than assert a name that could mislabel a live float, those stay reachable raw as `channel_<hex>` from
25
+ * the device's telemetry (a static members table cannot enumerate dynamic hex tags); each is promoted to
26
+ * a member here, one line, as a capture confirms its binding.
27
+ *
28
+ * @internal — the declaration `SolixEnergyMeterReads` derives from; exported (like the eufy `*_MEMBERS`
29
+ * tables) so it is a known symbol, but excluded from the rendered API reference.
30
+ */
31
+ export declare const SOLIX_ENERGY_METER_MEMBERS: {
32
+ /**
33
+ * Line-1 voltage (V), ff09 tag `0xAC` — the ONE confirmed meter binding, matched against a live
34
+ * single-phase frame (a nominal mains voltage). Read-only; the evidence gate installs its getter only
35
+ * once a frame carrying `0xAC` has landed, so it is absent (not a fabricated `0`) until then.
36
+ */
37
+ readonly meterVoltageL1: {
38
+ readonly param: 172;
39
+ readonly type: "number";
40
+ readonly kind: "scalar";
41
+ readonly unit: "V";
42
+ readonly provenance: "verified";
43
+ readonly description: "Meter line-1 voltage (V) — ff09 tag 0xAC, confirmed against a live single-phase frame.";
44
+ };
45
+ };
46
+ /** Bound `energyMeter` reads (the members-derived half of `dev.energyMeter()`). Read-only. */
47
+ export type SolixEnergyMeterReads = Surface<typeof SOLIX_ENERGY_METER_MEMBERS>;
48
+ /**
49
+ * The capabilities each Anker catalog category implies. Category is a detection SIGNAL (like eufy's
50
+ * `deviceTypes`), not the model's identity — a device still resolves `energyMeter` from its product code
51
+ * even though its category is "Accessory", which is why that category maps to nothing on its own.
52
+ * Unlisted categories contribute nothing here.
53
+ */
54
+ export declare const CATEGORY_CAPABILITIES: Readonly<Record<string, readonly SolixCapability[]>>;
55
+ /** Product-code prefixes known to be grid/energy meters (detects `energyMeter` regardless of category). */
56
+ export declare const SOLIX_METER_MODELS: readonly string[];
57
+ /**
58
+ * Product-code prefixes for the grid-tie Solarbank / home-battery family (detects `battery` +
59
+ * `solarInput` regardless of category): A1790 = Solarbank E1600 gen-1, A17C* = Solarbank 2 / 3.
60
+ */
61
+ export declare const SOLARBANK_MODELS: readonly string[];
62
+ /** The minimum device shape {@link detectSolixCapabilities} reads. */
63
+ export interface SolixDetectionInput {
64
+ product_code: string;
65
+ device_sw_version?: string;
66
+ wifi_online?: boolean;
67
+ wifi_name?: string;
68
+ rssi?: string | number;
69
+ }
70
+ /**
71
+ * Resolve a Solix device's capability set from its record fields, catalog category, and product-code
72
+ * prefix — the Solix analogue of eufy's `detectCapabilities`, kept Solix-scoped so eufy detection is
73
+ * untouched. `identity` is universal; the rest are OR-ed evidence.
74
+ */
75
+ export declare function detectSolixCapabilities(rec: SolixDetectionInput, category?: string): Set<SolixCapability>;
@@ -41,12 +41,19 @@ export interface DetectionSpec {
41
41
  *
42
42
  * eufy ships several ecosystems that share a cloud account and nothing else: `security` (cameras,
43
43
  * stations, locks, sensors — P2P plus the security-scoped broker), `life` (the T8L0x smart-lighting
44
- * line — its own credential and its own DP wire), and `clean` (robot vacuums — Tuya data points).
44
+ * line — its own credential and its own DP wire), `clean` (robot vacuums — Tuya data points) and
45
+ * `display` (the T87Ax Smart Display — secure MQTT, never P2P, its own 8001-8006 param space).
45
46
  * They overlap in retail vocabulary but share no wire, no param space and no semantics.
46
47
  *
48
+ * `display` is a line of its own for the second of those reasons rather than the first: without it,
49
+ * every security capability detected by a NAME regex is attachable to a Smart Display — measured at six,
50
+ * on a device that can answer for none of them because it speaks no P2P at all. A line holding one
51
+ * capability still buys that, which is why the count is not the measure of whether a line is worth
52
+ * declaring.
53
+ *
47
54
  * `any` is for the handful of capabilities that are genuinely line-independent (device identity).
48
55
  */
49
- export type ProductLine = "security" | "life" | "clean" | "print" | "any";
56
+ export type ProductLine = "security" | "life" | "clean" | "print" | "display" | "any";
50
57
  /**
51
58
  * A structural subset of a P2P frame. Deliberately NOT `import`ed from `p2p/*` — keeping it
52
59
  * structural avoids a model→p2p cycle, and the real `P2PFrame` is assignable to it. It is the
@@ -162,8 +169,13 @@ export interface DecodedState {
162
169
  * the manifest path and the command path.
163
170
  */
164
171
  export interface AvailabilityContext {
165
- /** Resolved codec/family. */
166
- codec: Codec;
172
+ /**
173
+ * Resolved codec/family. Absent for a device outside the eufy device model entirely — the codecs are
174
+ * the eufy transport families, so an ecosystem with its own backend has no truthful value here and
175
+ * says so by omission rather than borrowing another family's. Every gate that reads it compares
176
+ * against a specific codec, so an absent one matches none.
177
+ */
178
+ codec?: Codec;
167
179
  /** eufy DeviceType, when known. */
168
180
  deviceType?: number;
169
181
  /** Model / T-code, when known. */
@@ -192,35 +192,27 @@ export declare const ModeCtrlMethod: {
192
192
  readonly STOP_SMART_FOLLOW: 18;
193
193
  readonly START_GLOBAL_CRUISE: 20;
194
194
  };
195
- /**
196
- * The methods that carry a `Param` oneof — room, zone, goto, schedule, cruise and scene cleans.
197
- *
198
- * Deliberately absent from {@link ModeCtrlMethod}. Each needs an argument the caller has to supply and
199
- * this SDK cannot yet answer: a room or zone id comes from map data, which is not decodable here, and a
200
- * coordinate is signed centimetres in a frame no capture has pinned. Listing their numbers beside the
201
- * parameterless ones would invite a caller to send one with an empty payload, which is a valid frame
202
- * meaning something nobody intended.
203
- */
204
- /**
205
- * Encode a `ModeCtrlRequest` protobuf (DP 152) as a DP value: `varint(bodyLen) ++ {method:1, seq:2}`.
206
- *
207
- * Built on {@link RawDpWriter} rather than hand-rolled bytes. The frame is unchanged and the existing
208
- * byte-level test is what proves it — that test was written against a live T2351 capture, so it holds
209
- * the writer to the wire rather than to this function's own idea of the wire.
210
- *
211
- * Method 0 (START_AUTO_CLEAN) is omitted rather than written as an explicit zero, per the proto3
212
- * default-field rule and confirmed on that same capture. The writer deliberately does not apply that
213
- * rule itself: whether an explicit zero and an absent field mean the same thing is the
214
- * message's business, not the encoder's.
215
- * @internal
216
- */
217
195
  /**
218
196
  * The area-selecting `ModeCtrlRequest` methods, and the `Param` field each one's payload rides in.
219
197
  *
220
198
  * Kept apart from {@link ModeCtrlMethod} because these are a different kind of thing: a parameterless
221
199
  * verb is complete on its own, whereas each of these is meaningless without an argument the caller has
222
200
  * to supply. Sending one with an empty payload is a well-formed frame that means something nobody
223
- * intended, which is exactly why the numbers do not sit beside the others.
201
+ * intended, which is exactly why the numbers do not sit beside the others. Each verb built on one takes
202
+ * its argument in the signature: {@link VACUUM_CLEAN_MEMBERS.startScene},
203
+ * {@link VACUUM_CLEAN_MEMBERS.cleanRooms} and {@link VACUUM_CLEAN_MEMBERS.cleanZones}.
204
+ *
205
+ * The outer frame these ride in is byte-verified on a live T2351, and `SCENE`, `SELECT_ROOMS` and
206
+ * `SELECT_ZONES` have each since been RUN on a T2351 and did what they name — so their numbers rest on
207
+ * observed behaviour rather than on the vendor's definition alone.
208
+ *
209
+ * That distinction is the whole point of checking, and this is the one place it is argued: an AIoT
210
+ * data-point write is fire-and-forget, so a wrong number would be a different command arriving and
211
+ * looking exactly like success, which no frame check could catch. Watching the number is the only thing
212
+ * that rules it out.
213
+ *
214
+ * `GOTO` carries no encoder because a goto point is a coordinate no read on this SDK supplies, where a
215
+ * scene id and a map id both arrive on DP 180.
224
216
  */
225
217
  export declare const ModeCtrlParamMethod: {
226
218
  /** `START_SELECT_ROOMS_CLEAN` — clean the named rooms of a named map. */
@@ -267,7 +259,7 @@ export interface VacuumZoneTarget {
267
259
  * `mapId` is required and has no default, deliberately. The obvious shortcut is to assume the map a
268
260
  * single-floor home would have; on a two-floor home that silently sends the robot's ids against the
269
261
  * wrong floor's map. A caller that cannot name the map cannot safely make this call, and saying so is
270
- * better than picking for them.
262
+ * better than picking for them. {@link VACUUM_CLEAN_MEMBERS.cleanRooms} dispatches this.
271
263
  * @internal
272
264
  */
273
265
  export declare function encodeSelectRoomsClean(mapId: number, rooms: readonly VacuumRoomTarget[], cleanTimes?: number): string;
@@ -276,8 +268,25 @@ export declare function encodeSelectRoomsClean(mapId: number, rooms: readonly Va
276
268
  * @internal
277
269
  */
278
270
  export declare function encodeSelectZonesClean(mapId: number, zones: readonly VacuumZoneTarget[]): string;
279
- /** Build a scene clean, which needs only the scene's own id. @internal */
271
+ /**
272
+ * Build a scene clean, which needs only the scene's own id — `VacuumScene.id`, as DP 180 reports it.
273
+ * {@link VACUUM_CLEAN_MEMBERS.startScene} dispatches this.
274
+ * @internal
275
+ */
280
276
  export declare function encodeSceneClean(sceneId: number): string;
277
+ /**
278
+ * Encode a `ModeCtrlRequest` protobuf (DP 152) as a DP value: `varint(bodyLen) ++ {method:1, seq:2}`.
279
+ *
280
+ * Built on {@link RawDpWriter} rather than hand-rolled bytes. The frame is unchanged and the existing
281
+ * byte-level test is what proves it — that test was written against a live T2351 capture, so it holds
282
+ * the writer to the wire rather than to this function's own idea of the wire.
283
+ *
284
+ * Method 0 (START_AUTO_CLEAN) is omitted rather than written as an explicit zero, per the proto3
285
+ * default-field rule and confirmed on that same capture. The writer deliberately does not apply that
286
+ * rule itself: whether an explicit zero and an absent field mean the same thing is the
287
+ * message's business, not the encoder's.
288
+ * @internal
289
+ */
281
290
  export declare function encodeModeCtrl(method: number, seq: number): string;
282
291
  /**
283
292
  * Every value {@link VacuumActivity} can take, as data — the read's declared domain, so the schema a
@@ -1941,6 +1950,46 @@ export declare const VACUUM_CLEAN_MEMBERS: {
1941
1950
  readonly pauseCleaning: import("./members.js").MethodMember<() => Promise<void>> & {
1942
1951
  available: (ctx: import("./types.js").CommandContext) => boolean;
1943
1952
  };
1953
+ /**
1954
+ * Run a saved cleaning scene by its id (ModeCtrlRequest method 24 over DP 152).
1955
+ *
1956
+ * The id is the device's own, as {@link VACUUM_CLEAN_MEMBERS.scenes} reports it — `VacuumScene.id`
1957
+ * off the `SceneResponse` on DP 180. A scene the device reports invalid stays reportable and running
1958
+ * it is still a well-formed request; `VacuumScene.invalidReason` says why the device will refuse.
1959
+ *
1960
+ * Frame shape is byte-proven against the shared outer `ModeCtrlRequest`, and method 24 has been
1961
+ * WATCHED: run on a T2351, it started the named scene.
1962
+ */
1963
+ readonly startScene: import("./members.js").MethodMember<(sceneId: number) => Promise<void>> & {
1964
+ available: (ctx: import("./types.js").CommandContext) => boolean;
1965
+ };
1966
+ /**
1967
+ * Clean the named rooms of a named map (ModeCtrlRequest method 1 over DP 152).
1968
+ *
1969
+ * `mapId` has no default and that is deliberate: room ids are per map, so assuming the map a
1970
+ * single-floor home would have sends a two-floor home's ids against the wrong floor. `SceneInfo.mapid`
1971
+ * on DP 180 and a scheduled rooms-clean's `map_id` are the two real map ids the device reports.
1972
+ *
1973
+ * `cleanTimes` is how many passes to make over the set; rooms with no `order` are visited in the
1974
+ * order given.
1975
+ *
1976
+ * Frame shape is byte-proven against the shared outer `ModeCtrlRequest`, and method 1 has been
1977
+ * WATCHED: run on a T2351, it cleaned the rooms named.
1978
+ */
1979
+ readonly cleanRooms: import("./members.js").MethodMember<(mapId: number, rooms: readonly VacuumRoomTarget[], cleanTimes?: number) => Promise<void>> & {
1980
+ available: (ctx: import("./types.js").CommandContext) => boolean;
1981
+ };
1982
+ /**
1983
+ * Clean the given rectangles of a named map (ModeCtrlRequest method 2 over DP 152).
1984
+ *
1985
+ * Corners are SIGNED centimetres in the map's own frame, whose origin sits wherever the robot first
1986
+ * mapped from — negative coordinates are ordinary and are ZigZag-encoded, not written as plain
1987
+ * varints. Same `mapId` reasoning as {@link VACUUM_CLEAN_MEMBERS.cleanRooms}, and the same evidence:
1988
+ * method 2 was run on a T2351 and cleaned the rectangles given.
1989
+ */
1990
+ readonly cleanZones: import("./members.js").MethodMember<(mapId: number, zones: readonly VacuumZoneTarget[]) => Promise<void>> & {
1991
+ available: (ctx: import("./types.js").CommandContext) => boolean;
1992
+ };
1944
1993
  };
1945
1994
  /** `vacuum_clean` — core RoboVac scalar state + decoded activity: power, activity, volume, battery. */
1946
1995
  export declare const VACUUM_CLEAN: CapabilityModule;
@@ -30,3 +30,6 @@ export * from "./capabilities/index.js";
30
30
  */
31
31
  export type { FamilyContext } from "./device-family.js";
32
32
  export { CusPushEvent, CusPushAlarmType, CusPushMode, DoorbellPushEvent, IndoorPushEvent, HB3PairedDevicePushEvent, LockPushEvent, SmartDropPushEvent, NotificationStyle, detectionName, } from "./push-events.js";
33
+ export { SolixDevice, discoverSolixDevices, type SolixDeviceReader, type SolixDeviceRecord, type SolixIdentity, type SolixConnectivity, type SolixEnergyMeter, type SolixDeviceOptions, } from "./solix-device.js";
34
+ export { SOLIX_ENERGY_METER_MEMBERS, type SolixCapability, type SolixEnergyMeterReads } from "./capabilities/solix.js";
35
+ export { buildModelIndex, type SolixProduct, type SolixProductCategory } from "./solix-catalog.js";
@@ -26,3 +26,27 @@ export interface ParamDef {
26
26
  export declare const SECURITY_PARAMS: Record<number, ParamDef>;
27
27
  /** RoboVac Tuya DP space (ids ~150-180), from get_product_data_point. */
28
28
  export declare const CLEAN_PARAMS: Record<number, ParamDef>;
29
+ /**
30
+ * eufy Smart Display (T87Ax) param space — ids 8001-8006.
31
+ *
32
+ * Its own table rather than a corner of {@link SECURITY_PARAMS}: nothing in the 8000s carries a security
33
+ * meaning, so reading these ids there would decode a future security param assigned in this range as
34
+ * whatever it means on a camera.
35
+ *
36
+ * Every id here was reported by a live T87A0 (captured 2026-09-04). That the device SENT an id is what
37
+ * earns it a place in this table; the provenance label beside each one rates something narrower — how far
38
+ * its NAME is trusted. `modelName` and `modelCode` are `mega`, their values matching what the cloud
39
+ * record already carried. `battery` is `verified`: the id is real and the reading consistent, but the
40
+ * name came from the maintainer's own knowledge of the hardware rather than from the cloud data-point
41
+ * list, and `"100"` fits brightness, volume or charge equally.
42
+ *
43
+ * A dictionary entry is what makes a param readable by name off `getProperties()`. `capabilities/display.ts`
44
+ * decides separately which of them reach the typed surface, and only `battery` does.
45
+ *
46
+ * **8002 (`"1"`) and 8004 (a serial-shaped string) are absent, deliberately.** Neither meaning is
47
+ * legible from one value: `1` fits any enum or flag, and a serial could be the display's own or the
48
+ * station's it is bound to. They arrive as `unknown_8002` / `unknown_8004`, which is the measure of this
49
+ * list: it holds what is unknown, not what is unknowable. What would settle them: the vendor app's own
50
+ * display settings screen, one control at a time, params diffed after each.
51
+ */
52
+ export declare const DISPLAY_PARAMS: Record<number, ParamDef>;
@@ -14,7 +14,7 @@
14
14
  import type { Codec } from "./types.js";
15
15
  import { type ParamDef } from "./param-dictionary.js";
16
16
  /** The param id spaces this SDK models. */
17
- export type ParamNamespace = "security" | "clean" | "life" | "print";
17
+ export type ParamNamespace = "security" | "clean" | "life" | "print" | "display";
18
18
  /** Look up a param def in the given namespace. */
19
19
  export declare function paramDef(ns: ParamNamespace, paramType: number): ParamDef | undefined;
20
20
  /** The param namespace a device's ids live in, from its codec, via the module-local `NAMESPACE_BY_CODEC` table. */
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Anker Solix product catalog — the vendor's pairable-product registry (categories → products),
3
+ * used to label a discovered device's model code with a marketing name + category.
4
+ *
5
+ * This is model-layer vocabulary: pure data shapes + a lookup builder, with no wire/transport
6
+ * dependency. The catalog itself is fetched from the cloud by the client layer (`SolixClient`), which
7
+ * feeds it here via {@link buildModelIndex}; keeping the shapes and the index in the model layer lets
8
+ * `SolixDevice` resolve a name/category without reaching across the capability↔transport boundary.
9
+ */
10
+ export type { SolixProduct, SolixProductCategory } from "../core/solix-types.js";
11
+ import type { SolixProductCategory } from "../core/solix-types.js";
12
+ /**
13
+ * Flatten a product catalog into a `product_code → { name, category }` lookup for labelling
14
+ * discovered devices. Every variant code in `p_codes` maps to its parent product too, so a device
15
+ * reporting a sub-model resolves to the same marketing name.
16
+ */
17
+ export declare function buildModelIndex(categories: SolixProductCategory[]): Map<string, {
18
+ name: string;
19
+ category: string;
20
+ }>;