@mega-yfue/eufy-sdk 0.2.0-beta.4 → 0.2.0-beta.6

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.
@@ -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";
@@ -411,6 +412,8 @@ export interface DeviceActionMap {
411
412
  suction: SuctionActions;
412
413
  /** RoboVac locate (find-robot beep): `locating`; `locate(on?)`. */
413
414
  locate: LocateActions;
415
+ /** Smart Display (read-only): `battery`. No display write is captured. */
416
+ display: DisplayActions;
414
417
  /** Identity metadata (read-only): `{ manufacturer, model, serialNumber, name, deviceType?, firmwareVersion?, hardwareVersion? }`. */
415
418
  info: DeviceInfo;
416
419
  }
@@ -529,6 +532,7 @@ export { RTSP_MEMBERS } from "./rtsp.js";
529
532
  export { SIREN_MEMBERS } from "./siren.js";
530
533
  export { SMART_LIGHT_MEMBERS } from "./smart-light.js";
531
534
  export { SMOKE_MEMBERS } from "./smoke.js";
535
+ export { DISPLAY_MEMBERS, type DisplayActions } from "./display.js";
532
536
  export { SUCTION_MEMBERS } from "./suction.js";
533
537
  export { VACUUM_CLEAN_MEMBERS } from "./vacuum-clean.js";
534
538
  export type { DeviceInfo } from "./info.js";
@@ -550,7 +554,7 @@ export { HubAlarmTone, type HubAlarmToneValue } from "./siren.js";
550
554
  * RoboVac activity and clean type are the declared returns of the public `dev.vacuumClean()` getters,
551
555
  * so both unions are published.
552
556
  */
553
- export type { VacuumActivity, VacuumCleanType, CarpetStrategy, CleanExtent } from "./vacuum-clean.js";
557
+ export type { VacuumActivity, VacuumCleanType, CarpetStrategy, CleanExtent, VacuumRoomTarget, VacuumZoneTarget, } from "./vacuum-clean.js";
554
558
  /** The lists those unions are taken from — published because each union names its own. */
555
559
  export { VACUUM_ACTIVITIES, VACUUM_CLEAN_TYPES, CARPET_STRATEGIES, CLEAN_EXTENTS, MOP_LEVELS } from "./vacuum-clean.js";
556
560
  export { SuctionLevel, suctionLevelName, type SuctionLevelValue } from "./suction.js";
@@ -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
@@ -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;
@@ -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. */
@@ -28,10 +28,10 @@
28
28
  *
29
29
  * `display` is the T87Ax Smart Display line — its own codec because its `device_type` collides with
30
30
  * the security residual range (confirmed live, 2026-09-04: it connects over secure MQTT with no
31
- * `p2p_did`, never P2P). Its param namespace and product line are nonetheless grouped into `security`
32
- * by maintainer decision, not wire evidence — see {@link namespaceForCodec}. No capability module
33
- * targets it yet: no screen/audio/assistant param has been observed, only its own small cloud-param
34
- * namespace (ids 8001-8006).
31
+ * `p2p_did`, never P2P). It owns its own param namespace (ids 8001-8006) and its own product line, so
32
+ * no other line's capability can attach to it — see `namespaceForCodec` and `CODEC_LINE`. The `display`
33
+ * capability targets it and reads the charge; no screen/audio/assistant param has been observed, so
34
+ * those are absent rather than deferred.
35
35
  */
36
36
  export type Codec = "station" | "camera" | "sensor" | "lock" | "keypad" | "vacuum" | "mower" | "light" | "printer" | "display";
37
37
  /**
@@ -39,7 +39,7 @@ export type Codec = "station" | "camera" | "sensor" | "lock" | "keypad" | "vacuu
39
39
  * it maps to a {@link CapabilityModule} that owns its property schema. Extend this union as
40
40
  * new capabilities are modelled — adding one never requires a subclass.
41
41
  */
42
- export type Capability = "video" | "snapshot" | "motion" | "person_detection" | "battery" | "light" | "ptz" | "doorbell" | "contact" | "leak" | "smoke" | "co" | "siren" | "lock" | "keypad" | "arming" | "storage" | "rtsp" | "camera" | "audio" | "vacuum_clean" | "vacuum_dock" | "suction" | "locate" | "smart_light" | "info";
42
+ export type Capability = "video" | "snapshot" | "motion" | "person_detection" | "battery" | "light" | "ptz" | "doorbell" | "contact" | "leak" | "smoke" | "co" | "siren" | "lock" | "keypad" | "arming" | "storage" | "rtsp" | "camera" | "audio" | "vacuum_clean" | "vacuum_dock" | "suction" | "locate" | "smart_light" | "display" | "info";
43
43
  /** Value type of a property. */
44
44
  export type PropertyValueType = "bool" | "number" | "string" | "enum";
45
45
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mega-yfue/eufy-sdk",
3
- "version": "0.2.0-beta.4",
3
+ "version": "0.2.0-beta.6",
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",