@mega-yfue/eufy-sdk 0.0.4 → 0.1.0-beta.0

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 (143) hide show
  1. package/README.md +31 -12
  2. package/dist/client/device-registry.d.ts +343 -0
  3. package/dist/client/eufy-mega.d.ts +872 -0
  4. package/dist/client/index.d.ts +1 -6
  5. package/dist/client/map-channels.d.ts +22 -0
  6. package/dist/client/types.d.ts +383 -0
  7. package/dist/core/contracts.d.ts +898 -0
  8. package/dist/core/crypto.d.ts +98 -0
  9. package/dist/core/index.d.ts +9 -7
  10. package/dist/core/logger.d.ts +53 -0
  11. package/dist/core/lz4-block.d.ts +35 -0
  12. package/dist/core/raw-dp-hex.d.ts +32 -0
  13. package/dist/core/raw-dp-writer.d.ts +83 -0
  14. package/dist/core/store.d.ts +43 -0
  15. package/dist/core/types.d.ts +169 -0
  16. package/dist/core/util.d.ts +78 -0
  17. package/dist/index.d.ts +4 -7
  18. package/dist/index.js +25526 -14
  19. package/dist/index.js.map +7 -1
  20. package/dist/model/capabilities/access.d.ts +127 -0
  21. package/dist/model/capabilities/arming.d.ts +201 -0
  22. package/dist/model/capabilities/audio.d.ts +154 -0
  23. package/dist/model/capabilities/battery.d.ts +363 -0
  24. package/dist/model/capabilities/camera.d.ts +564 -0
  25. package/dist/model/capabilities/co.d.ts +40 -0
  26. package/dist/model/capabilities/contact.d.ts +133 -0
  27. package/dist/model/capabilities/doorbell.d.ts +345 -0
  28. package/dist/model/capabilities/dp-catalog.d.ts +38 -0
  29. package/dist/model/capabilities/index.d.ts +561 -0
  30. package/dist/model/capabilities/info.d.ts +28 -0
  31. package/dist/model/capabilities/keypad.d.ts +61 -0
  32. package/dist/model/capabilities/leak.d.ts +43 -0
  33. package/dist/model/capabilities/light.d.ts +174 -0
  34. package/dist/model/capabilities/locate.d.ts +63 -0
  35. package/dist/model/capabilities/lock.d.ts +242 -0
  36. package/dist/model/capabilities/manifest.d.ts +107 -0
  37. package/dist/model/capabilities/members.d.ts +647 -0
  38. package/dist/model/capabilities/motion.d.ts +377 -0
  39. package/dist/model/capabilities/person-detection.d.ts +8 -0
  40. package/dist/model/capabilities/ptz.d.ts +289 -0
  41. package/dist/model/capabilities/rtsp.d.ts +221 -0
  42. package/dist/model/capabilities/siren.d.ts +218 -0
  43. package/dist/model/capabilities/smart-light.d.ts +172 -0
  44. package/dist/model/capabilities/smoke.d.ts +40 -0
  45. package/dist/model/capabilities/snapshot.d.ts +6 -0
  46. package/dist/model/capabilities/storage.d.ts +11 -0
  47. package/dist/model/capabilities/suction.d.ts +104 -0
  48. package/dist/model/capabilities/types.d.ts +484 -0
  49. package/dist/model/capabilities/vacuum-clean.d.ts +1946 -0
  50. package/dist/model/capabilities/vacuum-dock.d.ts +208 -0
  51. package/dist/model/capabilities/video.d.ts +6 -0
  52. package/dist/model/classify.d.ts +78 -0
  53. package/dist/model/clean-record-detail.d.ts +65 -0
  54. package/dist/model/clean-records.d.ts +69 -0
  55. package/dist/model/device-family.d.ts +73 -0
  56. package/dist/model/device-types.d.ts +123 -0
  57. package/dist/model/device.d.ts +265 -0
  58. package/dist/model/index.d.ts +29 -4
  59. package/dist/model/infer.d.ts +23 -0
  60. package/dist/model/inspect.d.ts +61 -0
  61. package/dist/model/life-params.d.ts +21 -0
  62. package/dist/model/map-pixels.d.ts +70 -0
  63. package/dist/model/param-dictionary.d.ts +28 -0
  64. package/dist/model/param-namespace.d.ts +21 -0
  65. package/dist/model/proto-read.d.ts +53 -0
  66. package/dist/model/push-events.d.ts +147 -0
  67. package/dist/model/registry.d.ts +54 -0
  68. package/dist/model/types.d.ts +301 -0
  69. package/dist/model/vacuum-map-store.d.ts +92 -0
  70. package/dist/model/vacuum-map.d.ts +286 -0
  71. package/dist/model/vacuum-scenes.d.ts +76 -0
  72. package/dist/model/vacuum-schedules.d.ts +85 -0
  73. package/dist/transport/dp-preset.d.ts +102 -0
  74. package/dist/transport/ff09.d.ts +444 -0
  75. package/dist/transport/ffmpeg.d.ts +86 -0
  76. package/dist/transport/http/decodeImageV1.d.ts +20 -0
  77. package/dist/transport/http/decodeImageV2.d.ts +19 -0
  78. package/dist/transport/http/index.d.ts +5 -0
  79. package/dist/transport/http/light-catalog.d.ts +62 -0
  80. package/dist/transport/http/media-download.d.ts +14 -0
  81. package/dist/transport/http/mega-client.d.ts +514 -0
  82. package/dist/transport/http/phone-model.d.ts +21 -0
  83. package/dist/transport/index.d.ts +10 -7
  84. package/dist/transport/mqtt/app-client-id.d.ts +16 -0
  85. package/dist/transport/mqtt/availability.d.ts +14 -0
  86. package/dist/transport/mqtt/bare-ip-tls.d.ts +46 -0
  87. package/dist/transport/mqtt/biz-stream.d.ts +98 -0
  88. package/dist/transport/mqtt/broker-discovery.d.ts +55 -0
  89. package/dist/transport/mqtt/clean-codec.d.ts +14 -0
  90. package/dist/transport/mqtt/command-router.d.ts +285 -0
  91. package/dist/transport/mqtt/dp-codec.d.ts +58 -0
  92. package/dist/transport/mqtt/dp-color.d.ts +14 -0
  93. package/dist/transport/mqtt/engine.d.ts +16 -0
  94. package/dist/transport/mqtt/index.d.ts +5 -0
  95. package/dist/transport/mqtt/secure-mqtt.d.ts +107 -0
  96. package/dist/transport/mqtt/topics.d.ts +80 -0
  97. package/dist/transport/p2p/adts.d.ts +91 -0
  98. package/dist/transport/p2p/annexb.d.ts +124 -0
  99. package/dist/transport/p2p/codec.d.ts +166 -0
  100. package/dist/transport/p2p/command-router.d.ts +655 -0
  101. package/dist/transport/p2p/commands.d.ts +550 -0
  102. package/dist/transport/p2p/envelope.d.ts +46 -0
  103. package/dist/transport/p2p/fmp4.d.ts +89 -0
  104. package/dist/transport/p2p/fragment-recording.d.ts +33 -0
  105. package/dist/transport/p2p/index.d.ts +13 -0
  106. package/dist/transport/p2p/lan-ip.d.ts +25 -0
  107. package/dist/transport/p2p/live-stream.d.ts +223 -0
  108. package/dist/transport/p2p/live-trace.d.ts +123 -0
  109. package/dist/transport/p2p/media.d.ts +105 -0
  110. package/dist/transport/p2p/p2p-session.d.ts +620 -0
  111. package/dist/transport/p2p/readable-egress.d.ts +27 -0
  112. package/dist/transport/p2p/session-manager.d.ts +154 -0
  113. package/dist/transport/p2p/shared-live-source.d.ts +431 -0
  114. package/dist/transport/p2p/talkback.d.ts +187 -0
  115. package/dist/transport/p2p/video.d.ts +150 -0
  116. package/dist/transport/p2p/write-commands.d.ts +21 -0
  117. package/dist/transport/protobuf.d.ts +5 -0
  118. package/dist/transport/push/fcm.d.ts +23 -0
  119. package/dist/transport/push/index.d.ts +6 -0
  120. package/dist/transport/push/message-tags.d.ts +26 -0
  121. package/dist/transport/push/parser.d.ts +27 -0
  122. package/dist/transport/push/proto.d.ts +11 -0
  123. package/dist/transport/push/push-client.d.ts +64 -0
  124. package/dist/transport/push/store.d.ts +23 -0
  125. package/dist/transport/push/types.d.ts +180 -0
  126. package/dist/transport/raw-dp.d.ts +6 -0
  127. package/dist/transport/stored-image-cache.d.ts +23 -0
  128. package/dist/transport/tuya/account.d.ts +44 -0
  129. package/dist/transport/tuya/client.d.ts +89 -0
  130. package/dist/transport/tuya/command-router.d.ts +79 -0
  131. package/dist/transport/tuya/dp-codec.d.ts +56 -0
  132. package/dist/transport/tuya/index.d.ts +28 -0
  133. package/dist/transport/tuya/request.d.ts +157 -0
  134. package/dist/transport/tuya/sign.d.ts +64 -0
  135. package/package.json +12 -13
  136. package/dist/client/index.js +0 -2
  137. package/dist/client/index.js.map +0 -1
  138. package/dist/core/index.js +0 -2
  139. package/dist/core/index.js.map +0 -1
  140. package/dist/model/index.js +0 -2
  141. package/dist/model/index.js.map +0 -1
  142. package/dist/transport/index.js +0 -2
  143. package/dist/transport/index.js.map +0 -1
@@ -0,0 +1,208 @@
1
+ import type { RawDpCodec } from "../../core/contracts.js";
2
+ import type { ParamValue } from "../types.js";
3
+ import { type Surface } from "./members.js";
4
+ import type { AvailabilityContext, CapabilityModule } from "./types.js";
5
+ /**
6
+ * The DP 169 property name the ROBOT's own readings borrow, and the id carrying it.
7
+ *
8
+ * `DeviceInfo` is one message spanning two capabilities: the dock's firmware sits at field 11 and the
9
+ * robot's MAC, SSID and IP sit beside it. The one-owner rule is per product line, so one of the two has
10
+ * to own the id — this capability does, because it claimed it first and because moving it would break a
11
+ * shipped surface. The clean capability names this pair to read the rest. Exported so the far side
12
+ * cites a constant rather than retyping a string and a number that must agree.
13
+ */
14
+ export declare const VACUUM_DOCK_INFO_SOURCE: {
15
+ readonly property: "dockFirmwareVersion";
16
+ readonly param: 169;
17
+ };
18
+ /**
19
+ * Decode the DOCK's firmware version from `DeviceInfo` (DP 169).
20
+ *
21
+ * `undefined` covers every way it is not stated: no codec, a payload that does not decode, a robot not
22
+ * currently docked (no `station` block), or a dock that reports the block without a version. A
23
+ * length-delimited `string` arrives as bytes, so the value is read back as UTF-8.
24
+ * @internal
25
+ */
26
+ export declare function decodeDockFirmware(raw: ParamValue | undefined, codec: RawDpCodec | undefined): string | undefined;
27
+ /**
28
+ * Every value {@link DockActivity} can take — the read's declared domain, so the schema a caller reads
29
+ * and the type it compiles against are the same list rather than two that can drift.
30
+ *
31
+ * Published alongside {@link DockActivity} so the members are reachable as data at runtime, not only
32
+ * at compile time.
33
+ */
34
+ export declare const DOCK_ACTIVITIES: readonly ["idle", "washing", "drying", "descaling", "emptyingDust", "addingWater", "recyclingWater", "makingDisinfectant", "cuttingHair", "unknown"];
35
+ /**
36
+ * What the dock is doing — what `dev.vacuumDock()?.dockState` reports.
37
+ *
38
+ * A dock services several subsystems, so more than one can be busy at once; this answers the single
39
+ * most specific one — a subsystem that is running beats the mop system's own mode. `"unknown"` covers
40
+ * a state value outside the set the dock's own status message declares.
41
+ */
42
+ export type DockActivity = (typeof DOCK_ACTIVITIES)[number];
43
+ /**
44
+ * Decode a `StationResponse` (DP 173) Raw-DP value to the {@link DockActivity} the dock reports.
45
+ *
46
+ * Answers `undefined` for every way the dock has not stated an activity — an unbound device (no
47
+ * codec), a payload that does not decode, or one carrying no `status` message at all. That is distinct
48
+ * from `"idle"`, which is the dock actively saying it has nothing running, and from `"unknown"`, which
49
+ * is a state value this does not have a name for.
50
+ * @internal
51
+ */
52
+ export declare function decodeDockActivity(raw: ParamValue | undefined, codec: RawDpCodec | undefined): DockActivity | undefined;
53
+ /**
54
+ * Build a `StationRequest` carrying one manual dock command.
55
+ *
56
+ * Every one of these is the same two-level frame with a different inner field, so one builder serves
57
+ * them all and each member names only its command.
58
+ *
59
+ * **Reversed from the vendor's `station.proto`, NOT confirmed on a device.** Every member built on this
60
+ * carries `unverified`, so no setter is installed and the frame ships as documentation rather than as a
61
+ * callable control. An AIoT DP write is not refused by a router guard — it reaches the robot — and a
62
+ * fire-and-forget write that is wrong looks exactly like success, so the frame being plausible is not
63
+ * the bar. One `publishDps` capture per verb is what flips it.
64
+ * @internal
65
+ */
66
+ export declare function encodeStationCommand(command: number): string;
67
+ /**
68
+ * Every `vacuum_dock` feature, declared once.
69
+ *
70
+ * DP 173 is confirmed in the `get_product_data_point` catalog (raw, rw) as `baseStation`. The read
71
+ * side answers a typed {@link DockActivity} through {@link decodeDockActivity}. The write side
72
+ * (`StationRequest`) is a different message on the same DP and is not confirmed on a device — those
73
+ * members carry `unverified` with no `write` field, so no setter is installed and the intent path
74
+ * throws rather than guessing a frame.
75
+ * @internal
76
+ */
77
+ export declare const VACUUM_DOCK_MEMBERS: {
78
+ /**
79
+ * The DOCK's firmware version (DP 169, `DeviceInfo.station.software`) — distinct from
80
+ * `info.firmwareVersion`, which is the robot's and comes off the cloud device record.
81
+ *
82
+ * `undefined` while the robot is not docked: the vendor only fills the station block when the robot
83
+ * is powered on at the dock, so an absent value is normal rather than a fault.
84
+ */
85
+ readonly dockFirmwareVersion: {
86
+ readonly param: 169;
87
+ readonly type: "string";
88
+ readonly kind: "text";
89
+ readonly provenance: "mega";
90
+ readonly decode: (raw: unknown, codec: RawDpCodec | undefined) => string | undefined;
91
+ readonly decodedKind: "text";
92
+ readonly description: string;
93
+ };
94
+ /**
95
+ * What the dock is doing (DP 173, `StationResponse`) — washing or drying mops, emptying the bin,
96
+ * moving water, or idle.
97
+ *
98
+ * `undefined` means the dock has not stated an activity: the payload carried no `status` message, or
99
+ * the device is not bound to a codec. That is not the same as `"idle"`, which is the dock saying it
100
+ * has nothing running.
101
+ */
102
+ readonly dockState: {
103
+ readonly param: 173;
104
+ readonly type: "string";
105
+ readonly provenance: "mega";
106
+ readonly decode: (raw: unknown, codec: RawDpCodec | undefined) => "addingWater" | "cuttingHair" | "descaling" | "drying" | "emptyingDust" | "idle" | "makingDisinfectant" | "recyclingWater" | "unknown" | "washing" | undefined;
107
+ readonly decodedKind: "enum";
108
+ readonly decodedValues: readonly ["idle", "washing", "drying", "descaling", "emptyingDust", "addingWater", "recyclingWater", "makingDisinfectant", "cuttingHair", "unknown"];
109
+ readonly description: string;
110
+ };
111
+ /**
112
+ * Empty the dust bin into the dock. Write side of DP 173 — `StationRequest.manual_cmd.go_collect_dust`.
113
+ *
114
+ * The frame is built and reviewable; the member stays `unverified`, so no setter is installed and
115
+ * the intent path refuses it. What is missing is a capture, not the message shape.
116
+ */
117
+ readonly emptyDust: {
118
+ readonly type: "bool";
119
+ readonly kind: "boolean";
120
+ readonly writeOnly: true;
121
+ readonly unverified: true;
122
+ readonly write: () => import("../../core/contracts.js").Command;
123
+ readonly available: (ctx: AvailabilityContext) => boolean;
124
+ readonly provenance: "mega";
125
+ readonly description: "Empty the dust bin (DP 173 StationRequest.manual_cmd.go_collect_dust). Frame reversed from the vendor proto; unverified until captured on a device.";
126
+ };
127
+ /**
128
+ * Wash the mops in the dock — `StationRequest.manual_cmd.go_selfcleaning`. Same standing as
129
+ * {@link VACUUM_DOCK_MEMBERS.emptyDust}: frame built, not yet captured, so no setter is installed.
130
+ */
131
+ readonly washMops: {
132
+ readonly type: "bool";
133
+ readonly kind: "boolean";
134
+ readonly writeOnly: true;
135
+ readonly unverified: true;
136
+ readonly write: () => import("../../core/contracts.js").Command;
137
+ readonly available: (ctx: AvailabilityContext) => boolean;
138
+ readonly provenance: "mega";
139
+ readonly description: "Wash the mops (DP 173 StationRequest.manual_cmd.go_selfcleaning). Frame reversed from the vendor proto; unverified until captured on a device.";
140
+ };
141
+ /**
142
+ * Dry the mops in the dock — `StationRequest.manual_cmd.go_dry`. Frame built, not yet captured.
143
+ */
144
+ readonly dryMops: {
145
+ readonly type: "bool";
146
+ readonly kind: "boolean";
147
+ readonly writeOnly: true;
148
+ readonly unverified: true;
149
+ readonly write: () => import("../../core/contracts.js").Command;
150
+ readonly available: (ctx: AvailabilityContext) => boolean;
151
+ readonly provenance: "mega";
152
+ readonly description: "Dry the mops (DP 173 StationRequest.manual_cmd.go_dry). Frame reversed from the vendor proto; unverified until captured on a device.";
153
+ };
154
+ /**
155
+ * Run the dock's full deep self-clean cycle — `StationRequest.manual_cmd.self_maintain`.
156
+ *
157
+ * The longest-running of these — it occupies the dock for a while.
158
+ */
159
+ readonly selfMaintain: {
160
+ readonly type: "bool";
161
+ readonly kind: "boolean";
162
+ readonly writeOnly: true;
163
+ readonly unverified: true;
164
+ readonly write: () => import("../../core/contracts.js").Command;
165
+ readonly available: (ctx: AvailabilityContext) => boolean;
166
+ readonly provenance: "mega";
167
+ readonly description: "Run the dock's full self-maintenance cycle (DP 173 StationRequest.manual_cmd.self_maintain). Frame reversed from the vendor proto; unverified until captured on a device.";
168
+ };
169
+ /**
170
+ * Run the descaling cycle — `StationRequest.manual_cmd.go_remove_scale`. Only docks that make their
171
+ * own cleaning solution have this; the `available` gate is family-wide, so a device without it will
172
+ * simply ignore the frame.
173
+ */
174
+ readonly removeScale: {
175
+ readonly type: "bool";
176
+ readonly kind: "boolean";
177
+ readonly writeOnly: true;
178
+ readonly unverified: true;
179
+ readonly write: () => import("../../core/contracts.js").Command;
180
+ readonly available: (ctx: AvailabilityContext) => boolean;
181
+ readonly provenance: "mega";
182
+ readonly description: "Run the descaling cycle (DP 173 StationRequest.manual_cmd.go_remove_scale). Frame reversed from the vendor proto; unverified until captured on a device.";
183
+ };
184
+ /**
185
+ * Run the hair-cutting cycle on the brush — `StationRequest.manual_cmd.go_cut_hair`.
186
+ */
187
+ readonly cutHair: {
188
+ readonly type: "bool";
189
+ readonly kind: "boolean";
190
+ readonly writeOnly: true;
191
+ readonly unverified: true;
192
+ readonly write: () => import("../../core/contracts.js").Command;
193
+ readonly available: (ctx: AvailabilityContext) => boolean;
194
+ readonly provenance: "mega";
195
+ readonly description: "Run the hair-cutting cycle (DP 173 StationRequest.manual_cmd.go_cut_hair). Frame reversed from the vendor proto; unverified until captured on a device.";
196
+ };
197
+ };
198
+ /**
199
+ * Bound Omni dock controls — the object returned by `dev.vacuumDock()`.
200
+ *
201
+ * `dockState` reads as a typed {@link DockActivity}. All write members (`emptyDust`, `washMops`,
202
+ * `dryMops`) are `unverified` with no `write` field: the `StationRequest` wire is not confirmed on a
203
+ * device, so no setter appears on the surface until it is. The surface will fill out as writes are
204
+ * confirmed.
205
+ */
206
+ export type VacuumDockActions = Surface<typeof VACUUM_DOCK_MEMBERS>;
207
+ /** `vacuum_dock` — Omni dock controls (auto-empty, mop wash, mop dry) for the RoboVac X10 Pro Omni (T2351). */
208
+ export declare const VACUUM_DOCK: CapabilityModule;
@@ -0,0 +1,6 @@
1
+ import type { CapabilityModule } from "./types.js";
2
+ /**
3
+ * `video` — live/recorded streaming. Its value is *behaviour* (start/stop livestream,
4
+ * P2P media path), not reported state, so it contributes no state properties here.
5
+ */
6
+ export declare const VIDEO: CapabilityModule;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Device classifier — axis **B2** (command-codec family).
3
+ *
4
+ * ## What this module does
5
+ * Given a cloud device record, it answers one question: *which wire-protocol family
6
+ * does this device belong to?* — i.e. its {@link Codec} (`station | camera | sensor |
7
+ * lock | keypad`). It also exposes the baseline {@link Capability} set every member of a
8
+ * codec gets for free (before model rows / inference add device-specific extras).
9
+ *
10
+ * ## Why it is data-driven (no per-SKU table)
11
+ * eufy assigns every product a numeric **DeviceType** at the cloud, and that number — not
12
+ * the marketing model code — is the authoritative family discriminator. So the primary
13
+ * classifier ({@link codecForType}) is a small set of range / membership checks over the
14
+ * DeviceType space, with each group's DeviceType *names* spelled out in comments. A brand
15
+ * new camera SKU that ships with a DeviceType inside the camera range classifies correctly
16
+ * with zero code changes — that is the whole point of keeping this as data, not a wall of
17
+ * per-model predicates.
18
+ *
19
+ * The DeviceType groupings below are distilled from a third-party reverse-engineering
20
+ * project's device enum and its ~70 `isCamera`/`isStation`/`isSensor`/`isLock`/`isKeyPad`
21
+ * predicates, collapsed into a handful of numeric sets. That is a NAME/grouping source only —
22
+ * every behaviour keyed off a group is grounded in the V6 app or our own captures.
23
+ *
24
+ * ## Fallbacks
25
+ * When the cloud record has no DeviceType (older firmware, partial records), we fall back to
26
+ * a conservative regex over the model T-code ({@link codecFromModel}). {@link classify}
27
+ * chains the two and defaults to `"camera"` — the most common eufy-security device and a
28
+ * safe read-only default (a misrouted camera codec degrades to "video-ish" behaviour rather
29
+ * than, say, attempting lock actuation).
30
+ *
31
+ * @module model/classify
32
+ */
33
+ import type { Codec, CloudRecord } from "./types.js";
34
+ /**
35
+ * Map eufy's numeric **DeviceType** to a command {@link Codec}.
36
+ *
37
+ * Resolution order (first match wins): station → lock → sensor → keypad → camera. Anything
38
+ * that is a recognised security endpoint but not in the station/lock/sensor/keypad sets is
39
+ * treated as a **camera** (cameras, doorbells, floodlight/wall-light cams, garage cams,
40
+ * solocams, eufyCams, smart-drop, the LOCK_85V0 video-doorbell, etc.) — this is the large,
41
+ * fast-growing family, so it is the residual bucket rather than an explicit list.
42
+ *
43
+ * @param deviceType eufy DeviceType integer (e.g. `9` = CAMERA2, `54` = LOCK_8503 / R10).
44
+ * @returns the codec, or `undefined` for a genuinely unknown / non-finite input.
45
+ */
46
+ export declare function codecForType(deviceType: number): Codec | undefined;
47
+ /**
48
+ * Fallback classifier used when {@link CloudRecord.deviceType} is absent. Matches eufy
49
+ * product **T-codes** by family prefix. Deliberately conservative — it only claims a codec
50
+ * when the T-code range is a strong, unambiguous family signal; otherwise it returns
51
+ * `undefined` and lets {@link classify} apply the camera default.
52
+ *
53
+ * Known T-code families (case-insensitive):
54
+ * - `T85xx` / `T852x` / `T850x` → **lock** (Smart Lock R-series, video lock, etc.),
55
+ * EXCEPT `T8520`-prefixed which can be lock variants — still lock.
56
+ * - `T74xx` → **lock** (SmartSafe 7400-series).
57
+ * - `T80xx` / `T8001` / `T8002` / `T8010` / `T8030` / `T8023` / `T8025` → **station**
58
+ * (HomeBase / HomeBase 2 / 3 / Mini), and `T8N00` (NVR) / `T8E00` (PoE NVR) → station.
59
+ * - `T89xx` (entry/motion/water/siren sensors, e.g. T8900/T8910/T8920) → **sensor**.
60
+ * - `T87xx` keypad (`T8960`) → **keypad**.
61
+ * - any other `T8…` security T-code → **camera** (the default security family).
62
+ *
63
+ * @param model product/model code (T-code), e.g. `"T8423"`. `undefined`/empty → `undefined`.
64
+ * @returns the inferred codec, or `undefined` when the code is unrecognised.
65
+ */
66
+ export declare function codecFromModel(model: string | undefined): Codec | undefined;
67
+ /**
68
+ * Resolve a {@link CloudRecord} to its command {@link Codec}.
69
+ *
70
+ * Strategy: trust the cloud-provided numeric DeviceType first ({@link codecForType}); if
71
+ * that is absent/unknown, fall back to the model T-code ({@link codecFromModel}); if both
72
+ * fail, default to `"camera"` — the most common eufy-security device and a safe read-only
73
+ * default. **Always** returns a concrete codec.
74
+ *
75
+ * @param rec the minimal cloud record slice (deviceType + model are what matter here).
76
+ * @returns the resolved codec (never `undefined`).
77
+ */
78
+ export declare function classify(rec: CloudRecord): Codec;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The per-run detail blob a {@link CleanRecord} points at — map and statistics for one cleaning run.
3
+ *
4
+ * `get_device_clean_record_list` answers a list of runs, each carrying a `downloadUrl`. The bytes
5
+ * behind that URL are NOT a bare protobuf: they arrive inside the vendor's own container, with a magic
6
+ * header, a length, and a trailing checksum. This owns both halves — unwrapping the container and
7
+ * reading the message inside it — and neither fetches the URL nor decides whether it is safe to.
8
+ *
9
+ * Fetching stays with the caller on purpose: the host these URLs point at is unconfirmed, and this
10
+ * client's binary path is host-allowlisted with SSRF checks by design. Handing the SDK a URL to fetch
11
+ * would route around a control that exists for a reason.
12
+ *
13
+ * @module model/clean-record-detail
14
+ */
15
+ import type { RawDpCodec } from "../core/contracts.js";
16
+ /** Why a run ended, as the vendor's `finish_reason` reports it. */
17
+ export declare const CLEAN_FINISH_REASONS: readonly ["completed", "manual", "lowPower", "exception"];
18
+ export type CleanFinishReason = (typeof CLEAN_FINISH_REASONS)[number];
19
+ /** One completed run's statistics, decoded from its detail blob. */
20
+ export interface CleanRecordDetail {
21
+ /** When the run started, in unix SECONDS. */
22
+ readonly startTime: number;
23
+ /** When the run ended, in unix seconds. */
24
+ readonly endTime: number;
25
+ /** How long the run took, in seconds. */
26
+ readonly duration: number;
27
+ /** Area covered, in the unit the device reports it in (m² on every model seen so far). */
28
+ readonly area: number;
29
+ /** The cleaning type the run used, as the vendor's raw enum index. */
30
+ readonly cleanType: number;
31
+ /** Why the run ended. `undefined` when the device reported a value this version does not name. */
32
+ readonly finishReason: CleanFinishReason | undefined;
33
+ }
34
+ /**
35
+ * Unwrap the vendor's container and hand back the protobuf inside it, or `undefined`.
36
+ *
37
+ * The container is `0xAA 0x01`, a length, the message, then a two-byte big-endian checksum over every
38
+ * byte before it. **The width of the length field is not documented**, so rather than assume one, this
39
+ * tries the plausible widths and keeps the one whose checksum verifies — the checksum is the oracle,
40
+ * and a wrong guess about the length fails it rather than producing a plausible wrong message.
41
+ *
42
+ * That is the whole reason this validates instead of parsing optimistically: a run's statistics that
43
+ * are quietly wrong are worse than statistics a caller could not read.
44
+ */
45
+ export declare function unwrapCleanRecordBlob(blob: Uint8Array): Uint8Array | undefined;
46
+ /**
47
+ * Decode one run's detail blob into a {@link CleanRecordDetail}.
48
+ *
49
+ * `codec` is the same {@link RawDpCodec} the capabilities read DP payloads with — passed in rather
50
+ * than imported, because the implementation lives in the transport layer and this one does not reach
51
+ * across that line.
52
+ *
53
+ * Every field defaults to `0` when the message omits it: proto3 omits a zero, so a run that covered no
54
+ * area and one that said nothing about area are the same bytes, and `0` is the honest reading of both.
55
+ * `finishReason` is the exception — an index this version cannot name answers `undefined` rather than
56
+ * being flattened onto a neighbouring reason.
57
+ *
58
+ * Returns `undefined` when the container fails its checksum or the bytes inside are not a message —
59
+ * never throws, and never a partial read.
60
+ *
61
+ * **`Extra` is not decoded.** The message carries a nested `Extra { mode, mus, error_code, prompt_code }`
62
+ * whose own field number within `CleanRecordDesc` is not recorded in any source this SDK can point at.
63
+ * Reading it would mean picking a number, and a wrong one would silently report another field's bytes.
64
+ */
65
+ export declare function parseCleanRecordDetail(blob: Uint8Array, codec: RawDpCodec): CleanRecordDetail | undefined;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Cleaning-history records — the parsed result of a `get_device_clean_record_list` call.
3
+ *
4
+ * The vendor keeps one record per completed run: when it started, and a URL to a binary detail blob
5
+ * holding the run's map and statistics. This parses the LIST; the blob behind `downloadUrl` is not
6
+ * fetched here, because the host it points at has not been confirmed and the SDK's binary download
7
+ * path is host-allowlisted.
8
+ *
9
+ * Mirrors `capabilities/dp-catalog.ts`: the transport fetches the raw response and this owns the shape,
10
+ * so `model/` never learns how the call is signed and `transport/` never learns what a record means.
11
+ *
12
+ * @module model/clean-records
13
+ */
14
+ /** One completed cleaning run, as the cloud lists it. */
15
+ export interface CleanRecord {
16
+ /** The vendor's record id — what a delete call would name. */
17
+ readonly id: number;
18
+ /** When the run was recorded, in unix SECONDS (not milliseconds). */
19
+ readonly createTime: number;
20
+ /** When the record was last modified, in unix seconds. */
21
+ readonly updateTime: number;
22
+ /**
23
+ * URL of the run's binary detail blob — map and per-run statistics.
24
+ *
25
+ * Handed over rather than fetched: its host is unconfirmed, and the blob is a framed container
26
+ * rather than a bare protobuf, so decoding it needs evidence this SDK does not have yet.
27
+ */
28
+ readonly downloadUrl: string;
29
+ /** The vendor's storage path for the same blob. Empty when the response omits it. */
30
+ readonly filePath: string;
31
+ /**
32
+ * The vendor's opaque `extend` string, passed through verbatim.
33
+ *
34
+ * Its contents are not specified anywhere this SDK can point at, so it is deliberately NOT parsed —
35
+ * a guessed shape here would be a wire claim with no evidence behind it.
36
+ */
37
+ readonly extend: string;
38
+ }
39
+ /** A page of cleaning history, plus how many records exist in total. */
40
+ export interface CleanRecordPage {
41
+ /** The page as the cloud ordered it. Nothing here re-sorts, so the order is the gateway's contract. */
42
+ readonly records: readonly CleanRecord[];
43
+ /**
44
+ * Total records the account holds for this device — page through with `page` until it is reached.
45
+ *
46
+ * Falls back to this page's own record count when the response states no total, which can UNDERSTATE
47
+ * if a malformed record was skipped. That is the safe direction: a caller pages until it reaches the
48
+ * total, so a low figure ends the walk early rather than looping for records that never arrive.
49
+ */
50
+ readonly total: number;
51
+ }
52
+ /** Returned whenever the call fails or the response shape is not recognised. */
53
+ export declare const EMPTY_CLEAN_RECORD_PAGE: CleanRecordPage;
54
+ /**
55
+ * Parse a `get_device_clean_record_list` response into a {@link CleanRecordPage}.
56
+ *
57
+ * `raw` is the response's `data` object, already unwrapped by the transport — so the array sits at the
58
+ * top level under `clean_record_list`, beside `total`.
59
+ *
60
+ * A record without a usable `id` is skipped rather than admitted with a fabricated one: the id is what
61
+ * identifies the run, and a record that cannot be named is not a record a caller can act on. Every
62
+ * other field degrades to a default, so one odd entry never costs the whole page.
63
+ *
64
+ * `user_id` and `device_sn` are present in the response and deliberately not surfaced — the caller
65
+ * already knows which device it asked about, and the account id is not this type's business.
66
+ *
67
+ * Returns {@link EMPTY_CLEAN_RECORD_PAGE} on any shape mismatch — never throws.
68
+ */
69
+ export declare function parseCleanRecords(raw: unknown): CleanRecordPage;
@@ -0,0 +1,73 @@
1
+ /** The evidence a family decision needs — a structural subset of `CommandContext`. */
2
+ export interface FamilyContext {
3
+ /** eufy vendor DeviceType, when known (undefined ⇒ unknown ⇒ don't guess). */
4
+ deviceType?: number;
5
+ /** Model / T-code, when known. */
6
+ model?: string;
7
+ /**
8
+ * API category string — e.g. `"eufy_home"`, `"eufy_home_tuya"`, `"eufy_security"`. Supplied by
9
+ * the mega `get_devs_list` response; absent in unit-test contexts that build a minimal context
10
+ * without a real API record. Used as the PRIMARY transport discriminator for the clean line:
11
+ * `"eufy_home_tuya"` devices are on the ThingClips/Tuya Cloud platform, not Anker AIoT MQTT.
12
+ */
13
+ category?: string;
14
+ }
15
+ /** Indoor cams, incl. indoor pan/tilt + S350/E30/C-series + mini. */
16
+ export declare const INDOOR_CAMERA_TYPES: ReadonlySet<number>;
17
+ /** Indoor pan/tilt S350 family. */
18
+ export declare const INDOOR_PT_S350_TYPES: ReadonlySet<number>;
19
+ /** Outdoor pan/tilt + solo-PT. */
20
+ export declare const OUTDOOR_PT_TYPES: ReadonlySet<number>;
21
+ /** Floodlight cams. */
22
+ export declare const FLOODLIGHT_TYPES: ReadonlySet<number>;
23
+ /** Wall-light cams. */
24
+ export declare const WALL_LIGHT_TYPES: ReadonlySet<number>;
25
+ /**
26
+ * HomeBase-family hubs — the station-class devices with a built-in **speaker + alarm siren**, so
27
+ * they own the hub-audio surface (alarm / voice-prompt volume). A deliberate SUBSET of the `station`
28
+ * codec that EXCLUDES the NVRs (S4 Max, PoE NVR): those resolve to `station` for arming/storage but
29
+ * have no speaker, so they must NOT expose the hub-audio controls (they'd fire at nothing). The
30
+ * alarm/prompt wire is verified on HomeBase 3 (HB3); the other hubs share the hub hardware.
31
+ */
32
+ export declare const HOMEBASE_TYPES: ReadonlySet<number>;
33
+ /** Indoor camera (any indoor variant). */
34
+ export declare const isIndoorCamera: (ctx: FamilyContext) => boolean;
35
+ /** Indoor cost-down "mini" cam. */
36
+ export declare const isIndoorCamMini: (ctx: FamilyContext) => boolean;
37
+ /** Indoor pan/tilt S350 family. */
38
+ export declare const isIndoorPanTiltS350: (ctx: FamilyContext) => boolean;
39
+ /** Outdoor pan/tilt (+ solo-PT) family. */
40
+ export declare const isOutdoorPanTilt: (ctx: FamilyContext) => boolean;
41
+ /** Floodlight cam family. */
42
+ export declare const isFloodLight: (ctx: FamilyContext) => boolean;
43
+ /** HomeBase-family hub (has a speaker/alarm) — a station EXCLUDING the NVRs, per `HOMEBASE_TYPES`. */
44
+ export declare const isHomeBase: (ctx: FamilyContext) => boolean;
45
+ /** Wired doorbell (DeviceType.DOORBELL). */
46
+ export declare const isWiredDoorbell: (ctx: FamilyContext) => boolean;
47
+ /**
48
+ * Whether a vacuum uses the **Anker AIoT MQTT** transport (modern DP 150–180 protobuf scheme).
49
+ *
50
+ * A **negative exclusion**: returns `false` only for the one confirmed non-AIoT platform
51
+ * (`"eufy_home_tuya"` — ThingClips/Tuya Cloud). Any absent, unknown, or unrecognised category
52
+ * defaults to `true`, matching the polarity of `routeCommand` which sends `aiot-dp` to MQTT
53
+ * unless `category === "eufy_home_tuya"`. The two gates agree: an unknown-category AIoT
54
+ * vacuum both routes to MQTT *and* has its setters installed.
55
+ *
56
+ * | `category` | platform | returns |
57
+ * | ------------------- | ---------------------------------- | ------- |
58
+ * | `"eufy_home"` | Anker AIoT MQTT ✅ confirmed | `true` |
59
+ * | `"eufy_home_tuya"` | ThingClips/Tuya Cloud ✅ confirmed | `false` |
60
+ * | absent / any other | unknown — default to AIoT | `true` |
61
+ *
62
+ * Live-confirmed categories sourced from `get_devs_list` dumps: `"eufy_home_tuya"` from a T2266
63
+ * X8 Pro (2026-08-04). Additional category strings are added here as devices are captured.
64
+ */
65
+ export declare const isAiotVacuum: (ctx: FamilyContext) => boolean;
66
+ /**
67
+ * Whether a vacuum is on the **ThingClips/Tuya Cloud** platform (`"eufy_home_tuya"` category).
68
+ *
69
+ * The positive complement of the negative-exclusion {@link isAiotVacuum}: returns `true` only for
70
+ * the one confirmed non-AIoT platform. Used to extend capability `available` guards so Tuya
71
+ * vacuums receive the same write actions as AIoT ones, routed by the facade's `routeCommand`.
72
+ */
73
+ export declare const isTuyaVacuum: (ctx: FamilyContext) => boolean;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * eufy **DeviceType** numbers — the single named definition of the vendor's product-type space.
3
+ *
4
+ * A `device_type` is an immutable vendor fact (like a command id), NOT capability logic. It is
5
+ * defined ONCE here and referenced wherever a numeric type would otherwise be a magic number:
6
+ * codec routing (`classify.ts`) and each capability's detection/wire tables (e.g. `pan-tilt.ts`
7
+ * `detection.deviceTypes`, `light.ts` switch-format tables). Capability *rules* stay in the
8
+ * modules; only the number→name mapping lives here.
9
+ *
10
+ * This is the **eufy security** DeviceType space (P2P). The RoboVac / clean line is a separate
11
+ * Tuya ecosystem with no eufy device_type — it's classified by category + T2xxx model code, not by
12
+ * a number here (see `classify.ts`). Light-cams live here (e.g. WALL_LIGHT_CAM); a hypothetical
13
+ * pure Tuya bulb would sit outside this space like the vacuum.
14
+ *
15
+ * Merged from a third-party reverse-engineering project's device enum + the app's own
16
+ * `DeviceTypeConstants` — the app is the authority wherever the two disagree.
17
+ * A `const` object (not a TS `enum`) to match the codebase's `CommandType`/`P2P_CMD` style.
18
+ *
19
+ * @module model/device-types
20
+ */
21
+ export declare const DeviceType: {
22
+ readonly STATION: 0;
23
+ readonly CAMERA: 1;
24
+ readonly SENSOR: 2;
25
+ readonly FLOODLIGHT: 3;
26
+ readonly CAMERA_E: 4;
27
+ readonly DOORBELL: 5;
28
+ readonly BATTERY_DOORBELL: 7;
29
+ readonly CAMERA2C: 8;
30
+ readonly CAMERA2: 9;
31
+ readonly MOTION_SENSOR: 10;
32
+ readonly KEYPAD: 11;
33
+ readonly CAMERA2_PRO: 14;
34
+ readonly CAMERA2C_PRO: 15;
35
+ readonly BATTERY_DOORBELL_2: 16;
36
+ readonly HB3: 18;
37
+ readonly CAMERA3: 19;
38
+ readonly WATER_FREEZE_SENSOR_8920: 20;
39
+ readonly SIREN_SENSOR: 21;
40
+ readonly SMOKE_SENSOR: 22;
41
+ readonly CAMERA3C: 23;
42
+ readonly PROFESSIONAL_247: 24;
43
+ readonly MINIBASE_CHIME: 25;
44
+ readonly CAMERA3_PRO: 26;
45
+ readonly HOMEBASE_MINI: 28;
46
+ readonly INDOOR_CAMERA: 30;
47
+ readonly INDOOR_PT_CAMERA: 31;
48
+ readonly SOLO_CAMERA: 32;
49
+ readonly SOLO_CAMERA_PRO: 33;
50
+ readonly INDOOR_CAMERA_1080: 34;
51
+ readonly INDOOR_PT_CAMERA_1080: 35;
52
+ readonly FLOODLIGHT_CAMERA_8422: 37;
53
+ readonly FLOODLIGHT_CAMERA_8423: 38;
54
+ readonly FLOODLIGHT_CAMERA_8424: 39;
55
+ readonly INDOOR_OUTDOOR_CAMERA_1080P_NO_LIGHT: 44;
56
+ readonly INDOOR_OUTDOOR_CAMERA_2K: 45;
57
+ readonly INDOOR_OUTDOOR_CAMERA_1080P: 46;
58
+ readonly FLOODLIGHT_CAMERA_8425: 47;
59
+ readonly OUTDOOR_PT_CAMERA: 48;
60
+ readonly CAMERA_E40: 49;
61
+ readonly LOCK_BLE: 50;
62
+ readonly LOCK_WIFI: 51;
63
+ readonly LOCK_BLE_NO_FINGER: 52;
64
+ readonly LOCK_WIFI_NO_FINGER: 53;
65
+ readonly LOCK_8503: 54;
66
+ readonly LOCK_8530: 55;
67
+ readonly LOCK_85A3: 56;
68
+ readonly LOCK_8592: 57;
69
+ readonly LOCK_8504: 58;
70
+ readonly SOLO_CAMERA_SPOTLIGHT_1080: 60;
71
+ readonly SOLO_CAMERA_SPOTLIGHT_2K: 61;
72
+ readonly SOLO_CAMERA_SPOTLIGHT_SOLAR: 62;
73
+ readonly SOLO_CAMERA_SOLAR: 63;
74
+ readonly SOLO_CAMERA_C210: 64;
75
+ readonly FLOODLIGHT_CAMERA_8426: 87;
76
+ readonly SOLO_CAMERA_E30: 88;
77
+ readonly CAMERA_S4: 89;
78
+ readonly SMART_DROP: 90;
79
+ readonly BATTERY_DOORBELL_PLUS: 91;
80
+ readonly DOORBELL_SOLO: 93;
81
+ readonly BATTERY_DOORBELL_PLUS_E340: 94;
82
+ readonly BATTERY_DOORBELL_C30: 95;
83
+ readonly BATTERY_DOORBELL_C31: 96;
84
+ readonly SOLOCAM_E42: 98;
85
+ readonly INDOOR_COST_DOWN_CAMERA: 100;
86
+ readonly CAMERA_GUN: 101;
87
+ readonly CAMERA_SNAIL: 102;
88
+ readonly INDOOR_PT_CAMERA_S350: 104;
89
+ readonly INDOOR_PT_CAMERA_E30: 105;
90
+ readonly CAMERA_FG: 110;
91
+ readonly CAMERA_4G_S330: 111;
92
+ readonly SIREN_SENSOR_E20: 123;
93
+ readonly ENTRY_SENSOR_E20: 126;
94
+ readonly PIR_SENSOR_E20: 127;
95
+ readonly CAMERA_GARAGE_T8453_COMMON: 131;
96
+ readonly CAMERA_GARAGE_T8452: 132;
97
+ readonly CAMERA_GARAGE_T8453: 133;
98
+ readonly SMART_SAFE_7400: 140;
99
+ readonly SMART_SAFE_7401: 141;
100
+ readonly SMART_SAFE_7402: 142;
101
+ readonly SMART_SAFE_7403: 143;
102
+ readonly WALL_LIGHT_CAM: 151;
103
+ readonly SMART_TRACK_LINK: 157;
104
+ readonly SMART_TRACK_CARD: 159;
105
+ readonly TRACKER_87B4: 161;
106
+ readonly TRACKER_87B5: 162;
107
+ readonly LOCK_8502: 180;
108
+ readonly LOCK_8506: 184;
109
+ readonly LOCK_8531: 189;
110
+ readonly LOCK_85L0: 201;
111
+ readonly LOCK_85D0: 202;
112
+ readonly LOCK_85V0: 203;
113
+ readonly LOCK_85P0: 209;
114
+ readonly NVR_S4_MAX: 300;
115
+ readonly CAMERA_POE_S4: 301;
116
+ readonly WALL_LIGHT_CAM_81A0: 10005;
117
+ readonly INDOOR_8W11: 10006;
118
+ readonly INDOOR_PT_CAMERA_C220: 10008;
119
+ readonly INDOOR_PT_CAMERA_C210: 10009;
120
+ readonly INDOOR_PT_CAMERA_C220_V2: 10010;
121
+ readonly INDOOR_PT_CAMERA_C220_V3: 10011;
122
+ readonly CAMERA_C35: 10035;
123
+ };