@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,265 @@
1
+ /**
2
+ * The single, concrete {@link Device} class.
3
+ *
4
+ * There are **no subclasses**. A device's behaviour is entirely determined by its resolved
5
+ * `{ codec, capabilities, properties }` (see `resolveDevice`). Code asks
6
+ * `device.has("light")` / `device.getProperty("battery")` — never `instanceof FloodlightCamera`.
7
+ *
8
+ * Live state is held as a flat map of `PropertyValue`, updated by feeding raw param maps
9
+ * (from the cloud record or P2P notifications) into {@link Device.applyParams}. Params the model
10
+ * doesn't recognise are **not dropped** — they are kept as `unknown_<paramType>` passthrough, so
11
+ * coverage can grow later without losing data today (the graceful-unknown rule, applied to params).
12
+ *
13
+ * @module model/device
14
+ */
15
+ import type { Capability, CloudRecord, CommandContext, PropertyChange, PropertySpec, PropertyValue, ResolvedDevice } from "./types.js";
16
+ import type { CommandSink, MediaProvider, Ff09SettingsReader, RawDpCodec } from "../core/contracts.js";
17
+ import { type Logger } from "../core/logger.js";
18
+ import { type CapabilityAccessors, type DeviceManifest } from "./capabilities/index.js";
19
+ /** Prefix used for params that have no `PropertySpec` mapping yet. */
20
+ export declare const UNKNOWN_PARAM_PREFIX = "unknown_";
21
+ /** A raw param map as delivered by the cloud / P2P: param_type → raw value. */
22
+ export type RawParams = Record<number | string, string | number | boolean>;
23
+ /**
24
+ * A eufy device: one class, capability-driven. Construct from a resolved record (or a raw
25
+ * `CloudRecord` via {@link Device.fromRecord}), then feed it param updates.
26
+ */
27
+ export declare class Device {
28
+ /** Serial number (station/device SN). */
29
+ readonly sn: string;
30
+ /**
31
+ * The station this device's traffic belongs to: its parent HomeBase, or its own {@link sn} when it has none.
32
+ *
33
+ * Set from the record's `parentSn`, which is present only for a device that hangs off a base. A record that
34
+ * states none leaves the last known value, as every other identity field here does, so it starts at this
35
+ * device's own serial and every device therefore has a station.
36
+ */
37
+ stationSn: string;
38
+ /** Resolved command-codec family. */
39
+ codec: ResolvedDevice["codec"];
40
+ /** Resolved capability set. Widens if the device later reports evidence for more. */
41
+ capabilities: readonly Capability[];
42
+ /** Merged property schema (one entry per known property this device exposes). */
43
+ properties: readonly PropertySpec[];
44
+ /**
45
+ * What the user named this device in the app (`device_name`), falling back to {@link modelName} when
46
+ * the record carries none.
47
+ */
48
+ name: string;
49
+ /** Model / T-code from the record ("T8410"), when known. */
50
+ model?: string;
51
+ /** The model's display name ("Indoor Cam Pan & Tilt") — the product this unit is one of. */
52
+ modelName: string;
53
+ /** Which resolver tier produced the codec/caps. */
54
+ source: ResolvedDevice["source"];
55
+ /** Fast capability lookup. */
56
+ private capSet;
57
+ /** propertyName → spec, for applyParams. */
58
+ private specByName;
59
+ /**
60
+ * paramType → { spec, invert } for applyParams. Includes each spec's own `paramType` (invert
61
+ * from the spec) plus any `readAliases` (invert from the alias) so a property that rides
62
+ * different wire ids across device families still resolves to one named value.
63
+ */
64
+ private specByParam;
65
+ /** Which param namespace this device's ids live in (clean DPs vs security P2P). */
66
+ private namespace;
67
+ /** The record's `device_name` as stated, before the {@link modelName} fallback is applied. */
68
+ private deviceName?;
69
+ /** Live property values, keyed by property name (or `unknown_<pt>`). */
70
+ private readonly state;
71
+ /**
72
+ * Bound action objects per capability the device HAS, keyed by camelCased capability id
73
+ * (`light`, `ptz`, `camera`). Empty until {@link bindActions} runs — a bare model object
74
+ * (no network) has no actions. Populated by `EufyMega.getDevice`; surfaced through the fluent
75
+ * `dev.<cap>()` accessors ({@link CapabilityAccessors}).
76
+ */
77
+ private actionMap;
78
+ /**
79
+ * Whether {@link bindActions} has run — published through {@link describe} because an unbound device
80
+ * has no bound objects to enumerate, and "exposes nothing" and "not wired up yet" are different
81
+ * answers a caller has to be able to tell apart.
82
+ */
83
+ private bound;
84
+ /**
85
+ * Read-through freshness policy (injected by the facade via {@link setFreshnessPolicy}; the model
86
+ * stays transport-free — it only calls the supplied `refresh`). Default: caching OFF
87
+ * (`staleAfterMs = Infinity`), so a bare model object never triggers a fetch and existing behaviour
88
+ * is unchanged until the client wires a policy.
89
+ */
90
+ private staleAfterMs;
91
+ private refresher?;
92
+ /** Guards against firing more than one background refresh at a time (coalesces rapid stale reads). */
93
+ private refreshInFlight;
94
+ /**
95
+ * Host-supplied diagnostics sink. Defaults to {@link noopLogger} (silent) so a bare model object
96
+ * stays quiet; the facade passes its own `logger` through {@link fromRecord}. Used to WARN when a
97
+ * wire value doesn't match its declared `PropertySpec.type` (see {@link coerceByType}).
98
+ */
99
+ private readonly logger;
100
+ constructor(sn: string, resolved: ResolvedDevice, logger?: Logger);
101
+ /**
102
+ * Adopt a resolution: the capability set, the property schema, and everything derived from them.
103
+ *
104
+ * Shared by the constructor and {@link reresolve} so a widened device is indistinguishable from one
105
+ * that resolved that way to begin with — a second derivation path here would be a slow-drifting bug,
106
+ * since only the re-resolve case would exercise it.
107
+ */
108
+ private resolveInto;
109
+ /**
110
+ * Re-resolve against a fresher record and adopt the result if the capability set grew.
111
+ *
112
+ * A capability is granted on evidence the device reports, so a device that hadn't reported a param
113
+ * when it was first resolved lacks the capability that param proves — and would keep lacking it for
114
+ * the object's whole lifetime, even as the value itself started arriving. Re-resolving on fresh
115
+ * evidence closes that: the accessor appears, already bound if the device is bound.
116
+ *
117
+ * Only ever widens. A param the device stops reporting does not retract a capability, because the
118
+ * cloud record is a snapshot that can lose a field for reasons that have nothing to do with the
119
+ * hardware, and revoking an accessor a caller already holds is worse than keeping a quiet one.
120
+ *
121
+ * Returns the capabilities gained, empty when nothing changed — so a caller can skip re-binding.
122
+ */
123
+ reresolve(rec: CloudRecord): Capability[];
124
+ /**
125
+ * Wire this device to a {@link CommandSink} so its semantic action objects become live
126
+ * (`device.light?.on()`). `ctx` carries the evidence a capability uses to resolve the right
127
+ * command variant. Called by `EufyMega.getDevice`; a raw model object left unbound simply has
128
+ * no action objects (all accessors return `undefined`). `ff09Settings` reads that frame family's
129
+ * settings over whichever transport the device has; `rawDp` reads the structured payloads a few params
130
+ * carry in place of a scalar. Both are optional and both are named for the job, not the caller — a
131
+ * read needing one returns `undefined` without it. See `CapabilityModule.actions`'s doc before
132
+ * threading a third. The final `buildActions` arg is the live-state reader backing the capabilities' typed
133
+ * read getters (`dev.battery()?.level`): it closes over `this.getProperty`, so a getter built once
134
+ * here stays current as realtime/poll updates land in `this.state`.
135
+ */
136
+ bindActions(ctx: CommandContext, sink: CommandSink, media?: MediaProvider, ff09Settings?: Ff09SettingsReader, rawDp?: RawDpCodec): void;
137
+ /**
138
+ * Install the fluent capability accessors (`dev.ptz()`, `dev.light()`, …) once, for EVERY
139
+ * known accessor name — each reads `this.actionMap` live, so an accessor returns `undefined`
140
+ * (never "not a function") on an unbound device or one lacking the capability, and starts
141
+ * returning the action object after {@link bindActions}. Capability-agnostic: the names come
142
+ * from the barrel projection {@link accessorNamesFor}, so `device.ts` never names a
143
+ * capability. The single cast here is the only place the {@link CapabilityAccessors} types erase.
144
+ */
145
+ private installAccessors;
146
+ /** Build a Device from a raw cloud record (runs the 3-tier resolver). */
147
+ static fromRecord(sn: string, rec: CloudRecord, logger?: Logger): Device;
148
+ /**
149
+ * Take the unit's own identity — its name and model code — from the record.
150
+ *
151
+ * Separate from {@link resolveInto} because the resolver answers about the PRODUCT: it has no
152
+ * `device_name` to give, and re-running it must not replace "Dining room" with "Indoor Cam Pan &
153
+ * Tilt". Re-applied on every {@link reresolve} so a rename in the app lands on the next refresh.
154
+ *
155
+ * A record that omits a field is silent about it rather than asserting it went away — the same
156
+ * reasoning that keeps {@link reresolve} from retracting a capability — so an omission keeps the last
157
+ * known value and only a stated one replaces it.
158
+ */
159
+ private adoptIdentity;
160
+ /** Does this device have the given capability? */
161
+ has(cap: Capability): boolean;
162
+ /** Is the given property name part of this device's schema? */
163
+ hasProperty(name: string): boolean;
164
+ /**
165
+ * Wire a read-through freshness policy. When a cached property is older than `staleAfterMs`, a
166
+ * `getProperty`/`getProperties` read schedules ONE coalesced background `refresh()` (the facade
167
+ * supplies it, choosing the cheapest live transport) and returns the last-known value immediately —
168
+ * reads never block. Push / P2P realtime updates refresh `ts` themselves via {@link applyParams}, so
169
+ * a device kept fresh by realtime never re-fetches (a fresh entry is never stale). Keeps `model/`
170
+ * transport-free: the device only calls the injected callback.
171
+ */
172
+ setFreshnessPolicy(policy: {
173
+ staleAfterMs: number;
174
+ refresh: () => Promise<void>;
175
+ }): void;
176
+ /**
177
+ * Low-level property read by name — the untyped escape hatch. The typed fluent capability getters
178
+ * (`dev.battery()?.level`, `dev.contact()?.open`) answer the value already narrowed to its declared
179
+ * type where the property is exposed by a capability; this answers the loose `PropertyValue.value`
180
+ * (`boolean|number|string|object`). It is the only read for an unbound model object (no live client →
181
+ * no `dev.<cap>()`) and for a param not yet surfaced on a capability. Returns `undefined` if never
182
+ * observed.
183
+ */
184
+ getProperty(name: string): PropertyValue | undefined;
185
+ /**
186
+ * Snapshot of all current property values (named + unknown passthrough) — the untyped bulk read, for
187
+ * diagnostics / discovery. The typed fluent capability getters (`dev.battery()?.level`, …) answer one
188
+ * specific known property; this loose map answers every observed value at once. Under a freshness
189
+ * policy, schedules a background refresh when the OLDEST observed value is stale (one fetch covers
190
+ * every param) and returns the current snapshot immediately.
191
+ */
192
+ getProperties(): Record<string, PropertyValue>;
193
+ /**
194
+ * Fire the injected background refresh at most once at a time (coalescing rapid stale reads). The
195
+ * refresh calls {@link applyParams}, which updates each value's `ts` — so subsequent reads see fresh
196
+ * entries and stop re-triggering until the next staleness window. Fire-and-forget; never throws to
197
+ * the reader (a failed refresh just leaves the last-known value in place).
198
+ */
199
+ private scheduleRefresh;
200
+ /**
201
+ * Apply a raw param map (cloud record or P2P notification). Known params update their named
202
+ * property; unrecognised params are retained as `unknown_<paramType>` so nothing is lost.
203
+ *
204
+ * @param params param_type → raw value.
205
+ * @param ts observation time (epoch ms); defaults to `Date.now()`.
206
+ * @returns the list of property names whose value changed.
207
+ */
208
+ applyParams(params: RawParams, ts?: number): string[];
209
+ /**
210
+ * Which of these changed property names are worth ANNOUNCING, each with the value
211
+ * {@link getProperty} now serves for it — the second half of an {@link applyParams} call, and the input
212
+ * a facade turns into a property-change event.
213
+ *
214
+ * Only a name in this device's own schema survives, and EVERY name in it does. The schema is what the
215
+ * SDK published and {@link getProperty} serves every entry of, so announcing one is honest; a
216
+ * dictionary-named param and an `unknown_<paramType>` passthrough are things the SDK makes no claim
217
+ * about, and announcing either would promise a value it never agreed to serve. Diagnostics reach those
218
+ * through `inspectParams`.
219
+ *
220
+ * Nothing is withheld for being uninteresting, here or in a capability's own table. Which of a device's
221
+ * truths a host acts on is the host's call: a withheld value cannot be recovered, where an unwanted one
222
+ * costs a caller one comparison on the name.
223
+ *
224
+ * The value comes out of live state — written microseconds earlier by the same call that produced
225
+ * `changed` — through the same `narrow` the capability getters use, which is what makes an announcement
226
+ * and the getter beside it one answer rather than two. Not from the raw wire value: that is a second
227
+ * conversion and a second answer, which is exactly how a payload comes to disagree with its getter. And
228
+ * not by invoking the installed getter, which has read side effects (a scheduled background refresh, a
229
+ * codec call) an announcement must not trigger — and which an `unexposed` schema property does not have
230
+ * at all.
231
+ *
232
+ * Kept beside the state and the schema rather than in a caller, because both are here; a caller doing
233
+ * the join would be re-deriving what this object already holds. Says nothing about the previous value:
234
+ * a caller that needs the delta already holds it, because it was told last time.
235
+ */
236
+ announcements(changed: readonly string[]): PropertyChange[];
237
+ /**
238
+ * What this device exposes, as data — every installed read with what its value MEANS, every offerable
239
+ * action with what it accepts, and every event each capability emits.
240
+ *
241
+ * Beside {@link toJSON} rather than folded into it, because the two answer different questions and
242
+ * `toJSON` fires on every implicit `JSON.stringify` (an event payload, a log line) where the shape is
243
+ * not wanted. This one carries **shape only** — no values; read those through the capability getters
244
+ * it names.
245
+ *
246
+ * The reads and actions are the ones this device actually installed, so a caller can offer everything
247
+ * listed: a write the device gave no evidence for, or one whose wire is not confirmed, is absent
248
+ * rather than described. An unbound device (no live client) has nothing bound to enumerate and answers
249
+ * `bound: false` with empty `details`. `details` is resolved against this device's own facts (codec,
250
+ * model, capabilities), so a per-device enum domain (e.g. `workingMode`) carries the same options the
251
+ * property schema does.
252
+ */
253
+ describe(): DeviceManifest;
254
+ /** Plain-object view: serial, name, resolved codec/source, capabilities, and every current property value. */
255
+ toJSON(): Record<string, unknown>;
256
+ }
257
+ /**
258
+ * Declaration merge: give {@link Device} the fluent capability accessors (`dev.ptz()`,
259
+ * `dev.light()`, `dev.camera()`, …) with full IDE typing, WITHOUT `device.ts` naming a single
260
+ * capability. The accessor names + return types come from {@link CapabilityAccessors} (a projection
261
+ * of the capability modules in the barrel); `bindActions` installs the matching closures at runtime.
262
+ * Adding a capability adds an accessor here automatically — no edit to this file.
263
+ */
264
+ export interface Device extends CapabilityAccessors {
265
+ }
@@ -1,7 +1,32 @@
1
1
  /**
2
- * Device domain: the `Device` record, its capabilities, classification, events and parameters.
2
+ * Device model — public barrel.
3
3
  *
4
- * Describes what a device MEANS, never how it is spoken to. It never imports `transport/` or a wire
5
- * library.
4
+ * Data-driven, capability-based device model: one {@link Device} class, behaviour resolved from
5
+ * a {@link CloudRecord} via the 3-tier {@link resolveDevice} (model row → codec → inference).
6
+ *
7
+ * @module model
8
+ */
9
+ export * from "./types.js";
10
+ export { classify, codecForType, codecFromModel } from "./classify.js";
11
+ export { inferName } from "./infer.js";
12
+ export { MODEL_REGISTRY, resolveDevice } from "./registry.js";
13
+ export { Device, UNKNOWN_PARAM_PREFIX, type RawParams } from "./device.js";
14
+ export { SECURITY_PARAMS, CLEAN_PARAMS, type ParamDef } from "./param-dictionary.js";
15
+ export { LIFE_PARAMS } from "./life-params.js";
16
+ export { parseCleanRecords, EMPTY_CLEAN_RECORD_PAGE, type CleanRecord, type CleanRecordPage } from "./clean-records.js";
17
+ export { parseCleanRecordDetail, unwrapCleanRecordBlob, CLEAN_FINISH_REASONS, type CleanRecordDetail, type CleanFinishReason, } from "./clean-record-detail.js";
18
+ export { decodeVacuumSchedules, decodeVacuumScheduleCount, decodeActiveVacuumScheduleCount, VACUUM_SCHEDULE_ACTIONS, VACUUM_SCHEDULE_WEEKDAYS, type VacuumSchedule, type VacuumScheduleAction, type VacuumScheduleWeekday, } from "./vacuum-schedules.js";
19
+ export { decodeVacuumScenes, decodeVacuumSceneCount, decodeUsableVacuumSceneCount, SCENE_TYPES, SCENE_INVALID_REASONS, type VacuumScene, type SceneType, type SceneInvalidReason, } from "./vacuum-scenes.js";
20
+ export { decodeVacuumMap, decodeVacuumMapGeometry, decodeVacuumMapBackup, decodeVacuumMapDescription, decodeVacuumPose, decodeVacuumRestrictedZones, decodeVacuumRoomOutline, decodeVacuumRoomParams, DOCK_KINDS, FLOOR_TYPES, MAP_CELL_VALUES, MAP_FRAME_KINDS, MAP_QUALITIES, ROOM_SCENES, ROOM_SUCTIONS, type DockKind, type FloorType, type MapCellValue, type MapDock, type MapFrameKind, type MapLine, type MapPoint, type MapPose, type MapQuad, type MapQuality, type RoomScene, type RoomSuction, type VacuumMapBackup, type VacuumMapDescription, type VacuumMapGeometry, type VacuumMapPlane, type VacuumRestrictedZones, type VacuumRoom, type VacuumRoomOutline, type VacuumRoomParams, type VacuumRoomSettings, } from "./vacuum-map.js";
21
+ export { cellAtPoint, mapCellValue, mapCellValueAt, pointAtCell, roomAtPoint, roomIdAt, roomIdAtPoint, type MapCell, type PlacedPlane, } from "./map-pixels.js";
22
+ export { VacuumMapStore, type VacuumMapPiece, type VacuumMapSnapshot } from "./vacuum-map-store.js";
23
+ export { paramDef, namespaceForCodec, type ParamNamespace } from "./param-namespace.js";
24
+ export { inspectParams, type ParamInspection, type DeviceInspection } from "./inspect.js";
25
+ export * from "./capabilities/index.js";
26
+ /**
27
+ * The evidence a family gate reads, published because a member table's `available` predicate names it —
28
+ * `AUDIO_MEMBERS.alarmTone` gates on being a HomeBase, and that gate's parameter type is this. The
29
+ * DeviceType sets and the predicates themselves stay internal to `device-family.ts`.
6
30
  */
7
- export {};
31
+ export type { FamilyContext } from "./device-family.js";
32
+ export { CusPushEvent, CusPushAlarmType, CusPushMode, DoorbellPushEvent, IndoorPushEvent, HB3PairedDevicePushEvent, LockPushEvent, SmartDropPushEvent, NotificationStyle, detectionName, } from "./push-events.js";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Naming inference.
3
+ *
4
+ * Capability *detection* (evidence params, model/category hints, vendor device-type tables) lives
5
+ * per-capability in `src/model/capabilities/*` (each module's `detection` spec) and is dispatched by
6
+ * the barrel's `detectCapabilities`. What lives here is the one piece that is about *naming*, not
7
+ * capabilities: {@link inferName}.
8
+ *
9
+ * @module model/infer
10
+ */
11
+ import type { CloudRecord } from "./types.js";
12
+ /**
13
+ * Best-effort display name from a cloud record. Prefers a clean uppercase model T-code
14
+ * (e.g. `"T8423"`); falls back to `undefined`. Never throws.
15
+ *
16
+ * A "clean model code" is the eufy product-code shape: a leading letter (usually `T`) plus
17
+ * digits, optionally with a short alphanumeric suffix. We normalise case and trim noise; if
18
+ * the model doesn't look like a code at all we return `undefined` rather than echoing junk.
19
+ *
20
+ * @param rec The cloud device record.
21
+ * @returns The normalised model code, or `undefined` when none is usable.
22
+ */
23
+ export declare function inferName(rec: CloudRecord): string | undefined;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Device inspection — turn a real device's reported params into an enrichment report.
3
+ *
4
+ * Given a cloud record (model/deviceType/params), this produces:
5
+ * - the resolved `{ codec, capabilities, name }`,
6
+ * - a per-param table cross-referenced against the param dictionary (known/unknown, name,
7
+ * provenance, inferred type, live value),
8
+ * - a **paste-ready `registry.ts` row** for the device's model, and
9
+ * - **paste-ready param-dictionary snippets** for every param we don't yet know.
10
+ *
11
+ * Pure + offline (no network) so it is unit-testable; the live wrapper that fetches a device by
12
+ * serial lives on `EufyMega.inspectDevice`.
13
+ *
14
+ * @module model/inspect
15
+ */
16
+ import type { CloudRecord, PropertySource, PropertyValueType, RegistryEntry, ResolvedDevice } from "./types.js";
17
+ import { type ParamNamespace } from "./param-namespace.js";
18
+ /** One reported param, cross-referenced against our dictionary. */
19
+ export interface ParamInspection {
20
+ paramType: number;
21
+ /** Raw reported value (as delivered). */
22
+ value: string;
23
+ /** True when the param is in our dictionary for this namespace. */
24
+ known: boolean;
25
+ /** Our dictionary name, or `param<pt>` when unknown. */
26
+ name: string;
27
+ /** Trust of the dictionary mapping (absent ⇒ not in dictionary). */
28
+ provenance?: PropertySource;
29
+ /** Type inferred from the live value (bool/number/string). */
30
+ inferredType: PropertyValueType;
31
+ }
32
+ /** Full inspection report for one device. */
33
+ export interface DeviceInspection {
34
+ sn?: string;
35
+ model?: string;
36
+ deviceType?: number;
37
+ namespace: ParamNamespace;
38
+ resolved: ResolvedDevice;
39
+ params: ParamInspection[];
40
+ counts: {
41
+ total: number;
42
+ known: number;
43
+ unknown: number;
44
+ unconfirmed: number;
45
+ };
46
+ /** Suggested curated registry row (codec + extra caps + name). */
47
+ suggestedRegistry: {
48
+ model: string;
49
+ entry: RegistryEntry;
50
+ exists: boolean;
51
+ };
52
+ /** Paste-ready `registry.ts` line. */
53
+ registrySnippet: string;
54
+ /** Paste-ready param-dictionary lines for the unknown params. */
55
+ dictionarySnippet: string;
56
+ }
57
+ /**
58
+ * Inspect a cloud device record and produce the enrichment report. `rec.params` should be the
59
+ * device's reported `param_type → value` map (e.g. from the device list / get_device_param_list).
60
+ */
61
+ export declare function inspectParams(rec: CloudRecord, sn?: string): DeviceInspection;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `eufy_life` DP param space — the tag ids a T8L0x smart light reports on its own realtime wire.
3
+ *
4
+ * These are **DP tag numbers, not cloud param ids**, and they live in their own namespace because the
5
+ * numbers collide: tag `0xa3` = 163 means strip length here and battery level in the vacuum DP space.
6
+ * The read-side meanings are also unrelated to the write-side tags that share the same numbers on the
7
+ * outbound leg (`0xa1` is a timestamp there, power here) — device→app and app→device are two separate
8
+ * mappings over one tag range.
9
+ *
10
+ * CONFIRMED two ways (2026-07-28): decoded live off a real T8L02's status reports, matching the app's
11
+ * own `deviceInfoPayloadDataParse` transcription field for field. Each entry's DP tag and the app's
12
+ * field name for it are noted beside it.
13
+ *
14
+ * Names match the `PropertySpec.name`s in `capabilities/smart-light.ts` so a value resolves to the same
15
+ * property whether it arrives through this dictionary or the capability's own spec table.
16
+ *
17
+ * @module model/life-params
18
+ */
19
+ import type { ParamDef } from "./param-dictionary.js";
20
+ /** Tag ids a `eufy_life` light reports. Only fields whose meaning is evidenced appear here. */
21
+ export declare const LIFE_PARAMS: Record<number, ParamDef>;
@@ -0,0 +1,70 @@
1
+ import type { MapCellValue, MapPoint, VacuumMapPlane, VacuumRoom, VacuumRoomOutline, VacuumRoomParams } from "./vacuum-map.js";
2
+ /** A cell's position in a plane, counting from the plane's own origin corner. */
3
+ export interface MapCell {
4
+ readonly col: number;
5
+ readonly row: number;
6
+ }
7
+ /**
8
+ * What both planes have in common: a grid of a known size, anchored somewhere in the world.
9
+ *
10
+ * `VacuumMapGeometry` and `VacuumRoomOutline` both satisfy this, which is what lets one set of
11
+ * conversions serve both without either knowing about the other.
12
+ */
13
+ export interface PlacedPlane {
14
+ readonly width: number;
15
+ readonly height: number;
16
+ /** Centimetres per cell. `undefined` when the device omitted it, and then nothing here can convert. */
17
+ readonly resolution: number | undefined;
18
+ /** Where cell `(0, 0)` is anchored, in centimetres. */
19
+ readonly origin: MapPoint;
20
+ }
21
+ /**
22
+ * Which cell covers a world position, or `undefined` when the position falls outside the plane.
23
+ *
24
+ * Both axes count UP: a larger `y` is a larger `row`. The vendor's own renderer flips the image
25
+ * vertically before drawing it, which is a display choice and not a property of the data — reading the
26
+ * flip back into the coordinates would put every lookup in the wrong half of the map.
27
+ *
28
+ * `undefined` also when the plane has no `resolution`, because there is then no scale to divide by and
29
+ * a guessed one silently misplaces every lookup.
30
+ */
31
+ export declare function cellAtPoint(plane: PlacedPlane, point: MapPoint): MapCell | undefined;
32
+ /**
33
+ * Where a cell sits in the world, in centimetres — the inverse of {@link cellAtPoint}.
34
+ *
35
+ * The point returned is what the cell is anchored at, and `cellAtPoint` rounds to the nearest cell, so
36
+ * the two round-trip. `undefined` for a cell outside the plane or a plane with no resolution.
37
+ */
38
+ export declare function pointAtCell(plane: PlacedPlane, cell: MapCell): MapPoint | undefined;
39
+ /**
40
+ * What one cell of the map is — floor, wall, carpet or unexplored.
41
+ *
42
+ * The plane packs four cells to a byte, two bits each, low bits first, in one run with no row padding:
43
+ * the cell at `(col, row)` is at bit `(i & 3) * 2` of byte `i >> 2`, where `i = row * width + col`.
44
+ * `undefined` for a cell outside the map.
45
+ */
46
+ export declare function mapCellValue(map: VacuumMapPlane, cell: MapCell): MapCellValue | undefined;
47
+ /** What the map says about a world position. */
48
+ export declare function mapCellValueAt(map: VacuumMapPlane, point: MapPoint): MapCellValue | undefined;
49
+ /**
50
+ * Which room a cell of the ROOM OUTLINE belongs to, or `undefined` where none does.
51
+ *
52
+ * That plane is one byte per cell, and the byte is not the room id on its own: its low two bits carry
53
+ * a sub-type and the id is what remains above them. A reader taking the whole byte reports room 4 as
54
+ * room 19 and finds no such room in the list.
55
+ *
56
+ * A resulting id of `0` reads as `undefined`. Unmapped space and the gaps between rooms carry zero, and
57
+ * there is no room 0 to look up — reporting it as an id would have a caller searching the room list for
58
+ * something that was never in it.
59
+ */
60
+ export declare function roomIdAt(outline: VacuumRoomOutline, cell: MapCell): number | undefined;
61
+ /** Which room covers a world position. */
62
+ export declare function roomIdAtPoint(outline: VacuumRoomOutline, point: MapPoint): number | undefined;
63
+ /**
64
+ * The room at a world position, named — the join of the outline's room id with the room list.
65
+ *
66
+ * Takes the outline and the room list separately because the device sends them separately, on two
67
+ * channels that arrive at different times: a caller holding one without the other gets `undefined`
68
+ * rather than a wrong answer.
69
+ */
70
+ export declare function roomAtPoint(outline: VacuumRoomOutline, rooms: VacuumRoomParams, point: MapPoint): VacuumRoom | undefined;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * AUTHORITATIVE param dictionary — the app's own decompiled constant names joined with a live
3
+ * param-sweep of real owned devices.
4
+ *
5
+ * Two namespaces (params are per-transport, NOT globally unique):
6
+ * - SECURITY_PARAMS — eufy P2P param space (ids 1000+). **Membership is the observation**: an id is
7
+ * listed only because the sweep saw it on a real owned device, so the id is real/accepted.
8
+ * `provenance` is the trust of the NAME/meaning: "verified" (our captures) > "apk" (the app's own
9
+ * decompiled constant name) > "guessed" (no name source — needs toggle-diff).
10
+ * - CLEAN_PARAMS — RoboVac Tuya DP space (ids 1 and above), names from the cloud
11
+ * `get_product_data_point` data_point_list (provenance "mega" — authoritative).
12
+ *
13
+ * Which models reported an id, and the capture that named it, are in the commit that adds the entry.
14
+ */
15
+ import type { PropertyValueType, PropertySource, ParamEncoding } from "./types.js";
16
+ /** One param definition in the dictionary. */
17
+ export interface ParamDef {
18
+ /** Stable camelCase code-facing name. */
19
+ name: string;
20
+ type: PropertyValueType;
21
+ provenance: PropertySource;
22
+ /** How the wire value is encoded, when not a plain scalar (decode on read, encode on write). */
23
+ encoding?: ParamEncoding;
24
+ }
25
+ /** eufy P2P security param space (ids 1000+). */
26
+ export declare const SECURITY_PARAMS: Record<number, ParamDef>;
27
+ /** RoboVac Tuya DP space (ids ~150-180), from get_product_data_point. */
28
+ export declare const CLEAN_PARAMS: Record<number, ParamDef>;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Param namespaces — which id space a device's reported `paramType`s live in.
3
+ *
4
+ * The three spaces overlap numerically and mean unrelated things, so a lookup MUST be namespaced: id
5
+ * `163` is `battery` as a vacuum Tuya DP and `lightLength` as a `eufy_life` DP tag. Resolving a param
6
+ * without its namespace is how one product line's state silently reads as another's.
7
+ *
8
+ * This lives outside `param-dictionary.ts` so the namespace union and the dispatch stay one small table
9
+ * a reader can hold at once, next to the guard that a codec resolves to exactly one namespace — rather
10
+ * than at the head of two thousand entries.
11
+ *
12
+ * @module model/param-namespace
13
+ */
14
+ import type { Codec } from "./types.js";
15
+ import { type ParamDef } from "./param-dictionary.js";
16
+ /** The param id spaces this SDK models. */
17
+ export type ParamNamespace = "security" | "clean" | "life" | "print";
18
+ /** Look up a param def in the given namespace. */
19
+ export declare function paramDef(ns: ParamNamespace, paramType: number): ParamDef | undefined;
20
+ /** The param namespace a device's ids live in, from its codec, via the module-local `NAMESPACE_BY_CODEC` table. */
21
+ export declare function namespaceForCodec(codec: Codec): ParamNamespace;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The field readers every Raw-DP decoder on this line shares.
3
+ *
4
+ * `RawDpCodec` hands back a flat list of `{ field, kind, value }` and stops there — it has no schema
5
+ * and no idea which message it just walked. Turning that list into a reading is the decoder's job, and
6
+ * these are the readers every decoder on this line shares: the interesting part is not the code but the
7
+ * RULE each one encodes, and a rule spelled out in three places drifts in two of them.
8
+ *
9
+ * **The rule, in one line: proto3 omits a zero, so absent and zero are the same bytes.** A field set to
10
+ * `0`, `false` or `""` is not written to the wire at all, which means no reader here can tell "the
11
+ * device said zero" from "the device said nothing". Every function below therefore answers with the
12
+ * zero member rather than with `undefined`, and a decoder that needs the distinction has to find it
13
+ * somewhere else in the message — a `oneof` branch, a sibling flag, a wrapper message whose presence
14
+ * is itself the signal. Reading a defaulted zero as a real value is where this project's silent wire
15
+ * bugs have come from, three times over.
16
+ *
17
+ * @module model/proto-read
18
+ */
19
+ import type { RawDpCodec, RawDpField } from "../core/contracts.js";
20
+ /** Read a varint field as a number, or `0` — proto3 omits a zero, so absent and zero are one value. */
21
+ export declare function int(fields: readonly RawDpField[] | undefined, field: number): number;
22
+ /** Read a bool field on the same terms: absent is `false`, because that is what the vendor omitted. */
23
+ export declare function flag(fields: readonly RawDpField[] | undefined, field: number): boolean;
24
+ /**
25
+ * Read a `sint32`/`sint64` field, undoing the zig-zag encoding.
26
+ *
27
+ * The codec reads every varint unsigned, because the wire does not say which of `int32`, `sint32` and
28
+ * `uint32` a field was declared as — only the message's author knows, and that is the decoder. A
29
+ * `sint32` maps small negatives onto small unsigned values so they cost one byte instead of ten, and
30
+ * left alone they read as large positives: the map's coordinates are signed centimetres and routinely
31
+ * negative, so a point at `-150` would otherwise decode to `299` and place a wall on the wrong side of
32
+ * the room. This is the exact inverse of the writer's `zigzag`.
33
+ */
34
+ export declare function signed(fields: readonly RawDpField[] | undefined, field: number): number;
35
+ /**
36
+ * Read a string field, or `undefined`.
37
+ *
38
+ * An empty string reads as `undefined` too: proto3 omits it, so a device that sent no name and one
39
+ * that sent an empty name are the same bytes, and `""` is not a name.
40
+ */
41
+ export declare function text(fields: readonly RawDpField[] | undefined, field: number): string | undefined;
42
+ /**
43
+ * Read a length-delimited field as its raw bytes — a blob the sender did not intend as a message.
44
+ *
45
+ * Distinct from {@link sub} on purpose: a pixel plane and a nested message are the same wire type, and
46
+ * asking for one when the payload holds the other is how a decoder ends up walking image data as if it
47
+ * were fields. An empty payload reads as `undefined`, on the same proto3 rule as {@link text}.
48
+ */
49
+ export declare function bytes(fields: readonly RawDpField[] | undefined, field: number): Buffer | undefined;
50
+ /** Step into a sub-message, or `undefined` when the container is absent or is not one. */
51
+ export declare function sub(codec: RawDpCodec, fields: readonly RawDpField[] | undefined, field: number): readonly RawDpField[] | undefined;
52
+ /** Every repeat of one length-delimited field, stepped into — `find` would collapse a repeated one. */
53
+ export declare function each(codec: RawDpCodec, fields: readonly RawDpField[] | undefined, field: number): (readonly RawDpField[])[];