@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,484 @@
1
+ import type { DpCatalog } from "./dp-catalog.js";
2
+ /**
3
+ * Capability-module contract — the extended shape each `capabilities/<cap>.ts` file implements.
4
+ *
5
+ * A capability module is **self-contained**: it owns everything about one feature —
6
+ * - its **property schema** (the properties it contributes),
7
+ * - how the device is **detected** as having the capability ({@link DetectionSpec}),
8
+ * - how to **decode inbound frames** it cares about ({@link CapabilityModule.decodeFrame}),
9
+ * - how to **build outbound commands** ({@link CapabilityModule.buildCommand}).
10
+ *
11
+ * The barrel (`capabilities/index.ts`) collects the modules and exposes fleet-wide helpers
12
+ * (`detectCapabilities`, `decodeFrame`, `buildCommand`). Deleting a module file = removing one
13
+ * import line from the barrel; nothing else needs editing.
14
+ *
15
+ * @module model/capabilities/types
16
+ */
17
+ import type { Capability, Codec, CloudRecord, PropertySpec, PropertyValue, ValueKind } from "../types.js";
18
+ import type { Members, MemberDeps } from "./members.js";
19
+ import type { Command, DpInboundFrame } from "../../core/contracts.js";
20
+ /**
21
+ * How a capability is discovered on a device. All fields are additive OR-ed evidence — a device
22
+ * has the capability if ANY field matches. Mechanisms, most-to-least dynamic:
23
+ * - `evidenceParams` — a reported `param_type` whose PRESENCE proves the capability (the device
24
+ * self-reports it when its info is pulled over P2P). Namespace-agnostic: security param ids and
25
+ * vacuum Tuya-DP ids are declared the same way.
26
+ * - `deviceTypes` — vendor `DeviceType` numbers that guarantee the capability (static vendor
27
+ * table, e.g. Indoor-PT = 31/35/111). Used when there is no honest self-reported signal.
28
+ * - `modelHints` — regexes over the model / category / name strings.
29
+ * - `codecs` — the codec baseline already implies it (e.g. any `camera` has video).
30
+ * - `detect` — escape hatch for logic that doesn't fit the declarative fields. Must never throw.
31
+ */
32
+ export interface DetectionSpec {
33
+ evidenceParams?: number[];
34
+ deviceTypes?: number[];
35
+ modelHints?: RegExp[];
36
+ codecs?: Codec[];
37
+ detect?(rec: CloudRecord, codec: Codec): boolean;
38
+ }
39
+ /**
40
+ * Which product line a capability or a codec belongs to.
41
+ *
42
+ * eufy ships several ecosystems that share a cloud account and nothing else: `security` (cameras,
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).
45
+ * They overlap in retail vocabulary but share no wire, no param space and no semantics.
46
+ *
47
+ * `any` is for the handful of capabilities that are genuinely line-independent (device identity).
48
+ */
49
+ export type ProductLine = "security" | "life" | "clean" | "print" | "any";
50
+ /**
51
+ * A structural subset of a P2P frame. Deliberately NOT `import`ed from `p2p/*` — keeping it
52
+ * structural avoids a model→p2p cycle, and the real `P2PFrame` is assignable to it. It is the
53
+ * `p2p-frame` shape of {@link InboundSignal}.
54
+ */
55
+ export interface CapabilityFrame {
56
+ stationSn: string;
57
+ commandId: number;
58
+ channel: number;
59
+ data?: Buffer;
60
+ json?: {
61
+ cmd?: number;
62
+ payload?: unknown;
63
+ } & Record<string, unknown>;
64
+ }
65
+ /**
66
+ * A transport-neutral **inbound signal** — the dual of {@link Command}. Device events reach the
67
+ * SDK from three sources; a capability's {@link CapabilityModule.decodeEvent} normalizes ANY of
68
+ * them into one semantic {@link CapabilityEvent} (`motion`, `doorbellPress`, `lockState`,
69
+ * `ptzNotify`, …), so the consumer never has to know which transport delivered it:
70
+ * - `push` — an FCM notification, already normalized: a numeric `eventType` + optional thumbnail.
71
+ * - `p2p-frame` — a live P2P frame ({@link CapabilityFrame}).
72
+ * - `poll` — a cloud param that changed between polls (`paramType` from→to).
73
+ */
74
+ export type InboundSignal = {
75
+ source: "push";
76
+ eventType?: number;
77
+ eventName?: string;
78
+ deviceSn?: string;
79
+ stationSn?: string;
80
+ thumbnailUrl?: string;
81
+ payload: Record<string, unknown>;
82
+ } | ({
83
+ source: "p2p-frame";
84
+ } & CapabilityFrame) | {
85
+ source: "poll";
86
+ deviceSn: string;
87
+ paramType: number;
88
+ from?: string;
89
+ to?: string;
90
+ params: Record<number, string>;
91
+ } | {
92
+ source: "mqtt";
93
+ deviceSn?: string;
94
+ topic?: string;
95
+ raw: unknown;
96
+ frame?: DpInboundFrame;
97
+ dpParams?: Record<number, string>;
98
+ };
99
+ interface EventRefresh {
100
+ member: string;
101
+ }
102
+ /**
103
+ * A **declarative** inbound-event mapping — the dual of {@link DetectionSpec} for events. A
104
+ * capability lists which push `eventType`s / poll `paramType`s belong to it and the semantic event
105
+ * name each emits. The barrel folds all modules' mappings into one lookup index (built once), so
106
+ * dispatch is a direct id→event lookup — no per-module decode code for the common case.
107
+ */
108
+ export interface EventMapping {
109
+ /** Which source this id comes from. (p2p-frame decoding uses {@link CapabilityModule.decodeEvent}.) */
110
+ source: "push" | "poll";
111
+ /** An exact id, or an inclusive `[lo, hi]` range (e.g. lock push events 257..771). */
112
+ match: number | [number, number];
113
+ /** The semantic SDK event name to emit (e.g. `"motion"`, `"doorbellPress"`, `"lockState"`). */
114
+ emit: string;
115
+ /** @internal Refresh one reflected member before emitting a valueless transition. */
116
+ refresh?: EventRefresh;
117
+ /**
118
+ * Static fields folded into the emitted event payload — lets several ids emit the same event
119
+ * name with a discriminator (e.g. battery pushes 6/7/11 all → `batteryAlert` with
120
+ * `{state:"low"|"hot"|"full"}`). Merged OVER the signal's own fields, so a raw wire key can't
121
+ * overwrite a discriminator.
122
+ */
123
+ payload?: Record<string, unknown>;
124
+ /**
125
+ * Fields DERIVED from the signal — for state a push carries under an opaque single-letter wire key.
126
+ * Merged last, over both the raw body and {@link payload}.
127
+ *
128
+ * Same evidence bar as everything else: only map a key whose meaning is confirmed in the V6 app or a
129
+ * capture. Return `{}` when this signal doesn't carry the field, so nothing is invented.
130
+ */
131
+ derive?(signal: InboundSignal): Record<string, unknown>;
132
+ }
133
+ /**
134
+ * A decoded inbound event a capability wants surfaced on the SDK. `event` is the EufyMega event
135
+ * name (e.g. `"ptzNotify"`); `payload` is spread into the emitted object after `stationSn`.
136
+ */
137
+ export interface CapabilityEvent {
138
+ event: string;
139
+ payload: Record<string, unknown>;
140
+ }
141
+ /**
142
+ * Device state a capability recovered from an inbound realtime signal, expressed as `paramType →
143
+ * value` in the device's own param namespace — the same shape the cloud record reports, so it flows
144
+ * through `Device.applyParams` and reaches the typed {@link CapabilityModule.members} getters unchanged.
145
+ *
146
+ * Attribution stays with the signal: the transport already resolves which device a message came from,
147
+ * and a capability re-deriving it from the payload would be a second, disagreeable source of truth.
148
+ */
149
+ export interface DecodedState {
150
+ params: Record<number, string>;
151
+ }
152
+ /**
153
+ * Device context handed to {@link CapabilityModule.buildCommand} / {@link CapabilityModule.actions}
154
+ * so a capability can resolve the right command **variant** for THIS device — the same evidence
155
+ * `detection` keys on. Namespace-agnostic: `paramIds` holds security param ids OR vacuum Tuya DPs.
156
+ */
157
+ /**
158
+ * The device facts an availability / per-model gate reads. A truthful subset a {@link CloudRecord}
159
+ * can populate at resolve time — before a live session exists — without fabricating the transport
160
+ * fields ({@link CommandContext.channel}, {@link CommandContext.paramIds}) a real command carries.
161
+ * Every {@link CommandContext} is one structurally, so a gate written against this runs unchanged on
162
+ * the manifest path and the command path.
163
+ */
164
+ export interface AvailabilityContext {
165
+ /** Resolved codec/family. */
166
+ codec: Codec;
167
+ /** eufy DeviceType, when known. */
168
+ deviceType?: number;
169
+ /** Model / T-code, when known. */
170
+ model?: string;
171
+ /** API category string, when known. */
172
+ category?: string;
173
+ /** The device's resolved capability set, when known. */
174
+ capabilities?: ReadonlySet<Capability>;
175
+ /**
176
+ * Whether the device is reachable over P2P — a live-transport fact, so it is absent on the pure
177
+ * resolve-time (manifest) path and present only when a command context is built. Availability gates
178
+ * that read it (a lock's P2P-only writes) are all `writeOnly`, which the manifest never lists, so
179
+ * its absence there changes nothing.
180
+ */
181
+ hasP2p?: boolean;
182
+ /**
183
+ * The param_type / DP ids this device has actually reported. Present at bind time (a real
184
+ * `CommandContext`); absent on the manifest path. DP-based availability gates should treat
185
+ * `undefined` as an empty set — `ctx.paramIds?.has(dp) ?? false`.
186
+ */
187
+ paramIds?: ReadonlySet<number>;
188
+ }
189
+ export interface CommandContext extends AvailabilityContext {
190
+ /** Device channel (0 for standalone, `device_channel` on a HomeBase). */
191
+ channel: number;
192
+ /** eufy DeviceType, when known. */
193
+ deviceType?: number;
194
+ /** Model / T-code, when known. */
195
+ model?: string;
196
+ /**
197
+ * API category string — e.g. `"eufy_home"`, `"eufy_home_tuya"`, `"eufy_security"`. Primary
198
+ * transport discriminator for the clean line: `"eufy_home_tuya"` = ThingClips/Tuya Cloud, not
199
+ * Anker AIoT MQTT. Absent in unit-test contexts that build a minimal context without a real API.
200
+ */
201
+ category?: string;
202
+ /** Full device serial number, when known. */
203
+ serial?: string;
204
+ /** Display name, when known — sourced from the device record by the facade (for `info`). */
205
+ name?: string;
206
+ /**
207
+ * Firmware (main software) version — the device record's `main_sw_version`, the same field the v6
208
+ * app maps to its `firmware_main_version` label. `undefined` when the record doesn't carry it.
209
+ */
210
+ firmwareVersion?: string;
211
+ /**
212
+ * Hardware version — the device record's `main_hw_version` (app label `hardware_version`).
213
+ * `undefined` when the record doesn't carry it.
214
+ */
215
+ hardwareVersion?: string;
216
+ /** Secondary/sub firmware version — the record's `sec_sw_version` (app label `firmware_sub_version`). */
217
+ firmwareSubVersion?: string;
218
+ /** Wi-Fi MAC address — the record's `wifi_mac` (app label `mac_address`). */
219
+ macAddress?: string;
220
+ /** Firmware-update-available flag — the record's `needUpdate`. */
221
+ updateAvailable?: boolean;
222
+ /** The param_type / DP ids this device has actually reported (evidence for variant selection). */
223
+ paramIds: ReadonlySet<number>;
224
+ /**
225
+ * The capability set the device was RESOLVED to have (from `resolveDevice`: curated row +
226
+ * codec baseline + per-module detection over the full, fresh record). `buildCommand` gates on
227
+ * this so the authorization matches exactly what `device.has(cap)` / `buildActions` saw — never a
228
+ * weaker re-detection from a partial context. Omit only in unit tests that pass evidence directly.
229
+ */
230
+ capabilities?: ReadonlySet<Capability>;
231
+ /**
232
+ * The lock owner's account id — the identity a lock command is authenticated against. Present on
233
+ * lock-family devices; absent elsewhere.
234
+ */
235
+ adminUserId?: string;
236
+ /** The acting member's short id (`member.short_user_id`, hex, e.g. `"0003"`) — the lock cmd `A5` field. */
237
+ shortUserId?: string;
238
+ /** The logged-in account's display name (email local-part) — the lock cmd acting-username `A4` field. */
239
+ accountName?: string;
240
+ /**
241
+ * Whether the device has a usable P2P endpoint (a non-empty `p2p_did`). A HomeBase-attached lock
242
+ * (T8531) is P2P-reachable; a standalone garage/lock (T85D0, `p2p_did:""`) is MQTT-only. The lock
243
+ * capability uses this to route lock/unlock to P2P vs. reject with a clear MQTT-not-wired error.
244
+ */
245
+ hasP2p?: boolean;
246
+ /**
247
+ * Whether the device hangs off a HomeBase (a `parent_sn` other than its own) rather than standing
248
+ * alone. A DEVICE fact, not a transport one — the same class of routing evidence as {@link hasP2p}.
249
+ * The `rtsp` capability gates on it because a station serves an attached camera's stream itself and
250
+ * ignores that camera's authentication setting, so the write cannot do what its name promises there.
251
+ */
252
+ homeBaseAttached?: boolean;
253
+ /**
254
+ * Parsed `get_product_data_point` catalog for this device's SKU — present for vacuum/mower devices,
255
+ * absent for all other codecs. Capabilities use it for per-model feature-availability and value-range
256
+ * data (e.g. which suction levels DP 158 admits). Absent means "catalog not fetched" — fall back to
257
+ * static defaults rather than treating the device as incapable.
258
+ */
259
+ dpCatalog?: DpCatalog;
260
+ }
261
+ /**
262
+ * The **loose base** for a capability's bound action object — a bag of async methods. Each module
263
+ * declares its own precise, JSDoc'd alias (e.g. `PtzActions`, `CameraActions`) and returns THAT
264
+ * from `actions()`; those aliases are what the fluent `dev.<cap>()` accessors expose (assembled into
265
+ * `DeviceActionMap` in the barrel). This base only exists so `CapabilityModule.actions` has a common
266
+ * return type that every concrete alias is assignable to. Control actions resolve to `void` (they
267
+ * dispatch a {@link Command}); media actions resolve to data — a `Promise` of a still/stream/buffer,
268
+ * or a synchronous `AsyncIterable` (continuous fragment recording), so the return type is `any`.
269
+ *
270
+ * This models the common case: a **methods-bag** (control capabilities). A READ-ONLY *data*
271
+ * capability (`info`, returning a plain `DeviceInfo`) is the deliberate exception — it asserts its
272
+ * result to this type locally (see `info.ts`) rather than weaken this base for every module. The
273
+ * consumer-facing types stay precise via `DeviceActionMap` (`ptz: PtzActions`,
274
+ * `info: DeviceInfo`, …), from which the fluent `dev.<cap>()` accessors are derived.
275
+ */
276
+ export type CapabilityActions = Record<string, (...args: any[]) => any>;
277
+ /**
278
+ * Live-state accessor handed to {@link CapabilityModule.actions} so a capability can expose **typed
279
+ * read getters** on its fluent object (`dev.battery()?.level: number`) alongside its write methods —
280
+ * closing the read/write asymmetry (writes are typed+fluent; a raw `getProperty("battery")?.value` is
281
+ * the loose `ParamValue`). Returns the current property value for a property NAME, or
282
+ * `undefined` if never observed. The facade wires it to the device's live state, so a getter built
283
+ * once stays current as realtime/poll updates land. The `readNum`/`readBool`/`readStr` extractors in
284
+ * `./access.ts` guard the runtime type rather than lie-cast.
285
+ */
286
+ export type CapabilityStateReader = (name: string) => PropertyValue | undefined;
287
+ /**
288
+ * One argument a described action accepts, in the same value vocabulary the reads use ({@link ValueKind}).
289
+ *
290
+ * A numeric range belongs here as `min`/`max`/`step`, and must be the SAME constant the action clamps
291
+ * with: a retyped range is a drift bug no test can catch, since both copies stay individually valid.
292
+ *
293
+ * `values`/`labels` publish a fixed option set for an argument whose domain the schema cannot otherwise
294
+ * reach. A `kind: "enum"` argument normally omits both: it belongs to a control whose property already
295
+ * publishes its `enumValues`, and declaring the same set twice can only drift from it.
296
+ */
297
+ export interface ActionArgSpec {
298
+ name: string;
299
+ kind: ValueKind;
300
+ optional?: boolean;
301
+ min?: number;
302
+ max?: number;
303
+ step?: number;
304
+ values?: readonly (string | number)[];
305
+ labels?: Readonly<Record<string, string>>;
306
+ description?: string;
307
+ }
308
+ /**
309
+ * What one action on `dev.<cap>()` accepts and what it changes — the write-side counterpart to a
310
+ * member's read getter.
311
+ *
312
+ * Carries no name: it is attached to the method itself (`describedAction` in `./access.ts`), so the
313
+ * action's own key is its name and a rename cannot leave a description behind pointing at nothing.
314
+ *
315
+ * **A spec never gates anything.** {@link CapabilityModule.actions} alone decides which methods exist;
316
+ * an undescribed action stays fully callable, it just cannot be offered automatically.
317
+ *
318
+ * **Describe the method that TAKES the value, not its aliases.** `on()`/`off()` drive the same wire as
319
+ * `set(v)`, and describing all three would render three controls for one state.
320
+ *
321
+ * **A stateful action's {@link reflects} read is its evidence gate.** Each described write works exactly
322
+ * on the devices whose reflected read is installed — the read's backing param and the write's own
323
+ * precondition are the same evidence (`motion.setDetection` needs the PIR switch a standalone sensor
324
+ * never reports; `setHumanOnlyAtNight` and `setLoiteringDetection` each need the id their read gates on).
325
+ * So a description is a promise the method works wherever its read answers, and an action whose read is
326
+ * absent on a device is not offerable there.
327
+ *
328
+ * Describe only a wire confirmed on real hardware, for the same reason an unverified write rejects
329
+ * rather than guesses: a described control that always fails turns "a present method means a verified
330
+ * wire" into "a present description means nothing".
331
+ */
332
+ export interface ActionSpec {
333
+ form: "momentary" | "stateful";
334
+ /** For a stateful action, the read accessor on the same capability whose value it changes. */
335
+ reflects?: string;
336
+ args?: readonly ActionArgSpec[];
337
+ description?: string;
338
+ }
339
+ /**
340
+ * A capability module: property schema + detection + inbound decode + outbound commands. Written
341
+ * once, reused by every device that lists the capability.
342
+ */
343
+ export interface CapabilityModule {
344
+ capability: Capability;
345
+ /**
346
+ * Properties this capability contributes.
347
+ *
348
+ * A module with a {@link members} table sets this to `propertiesOf(ITS_MEMBERS)` rather than writing
349
+ * it out: the schema is derived from the same declaration as the getters and setters, and is
350
+ * materialised here so every existing consumer keeps reading it the same way.
351
+ */
352
+ properties: PropertySpec[];
353
+ /** Human description for docs. */
354
+ description?: string;
355
+ /** How devices are detected as having this capability. Absent = only via codec baseline / registry. */
356
+ detection?: DetectionSpec;
357
+ /**
358
+ * The product line this capability belongs to; defaults to `security` (the bulk of the catalogue).
359
+ *
360
+ * Checked BEFORE any detection evidence, so a capability can never land on a device from another
361
+ * line. This matters because the detection fields are OR-ed and several capabilities are found by
362
+ * NAME alone — eufy's retail vocabulary collides across ecosystems, so a light called "Outdoor
363
+ * Spotlights" or a strip named for a water effect otherwise picks up a camera or leak capability
364
+ * whose wire it does not speak.
365
+ */
366
+ line?: ProductLine;
367
+ /**
368
+ * The capability belongs to whichever device OWNS the group, so a device that hangs off a parent
369
+ * station must not claim it — even when a curated row grants it or the device mirrors the param.
370
+ *
371
+ * Guard mode is the case that grounds this: a standalone SoloCam/Indoor cam owns its own mode (read
372
+ * live: `armingMode` answers on a standalone T8170/T8171/T8410), but behind a HomeBase the hub owns
373
+ * it and the app shows it there, not per camera — an attached T8170 reports no mode at all. Applied
374
+ * AFTER the three tiers are unioned (see `resolveDevice`), because the point is to withhold a
375
+ * control the device cannot answer for however it was granted.
376
+ */
377
+ ownedByStation?: boolean;
378
+ /**
379
+ * Declarative inbound-event mappings (push eventType / poll param → semantic event name). The
380
+ * common case: pure data, no code. The barrel indexes these for direct lookup.
381
+ */
382
+ events?: EventMapping[];
383
+ /**
384
+ * Semantic event names this capability emits from {@link decodeEvent}, which the flat {@link events}
385
+ * table cannot express — a frame the module parses itself has no id to list there.
386
+ *
387
+ * Declared so the published manifest can state what a capability emits without a caller subscribing
388
+ * blind: the manifest's event list is this union'd with the table's own `emit` names. A capability
389
+ * whose events all come from the table omits it; `decode-event.spec.ts` locks the union against a
390
+ * hardcoded list, since the typed event map erases at build and cannot keep this honest.
391
+ */
392
+ emits?: readonly string[];
393
+ /**
394
+ * Semantic events that describe a **state**, and the payload field holding it — so a state that
395
+ * several transports report is announced once per real change instead of once per transport.
396
+ *
397
+ * One physical change can reach the SDK on more than one path: an entry sensor's contact arrives
398
+ * as a station notify ~2 s before the same value arrives as an FCM push. Both are real, but they
399
+ * describe one change. Listing the event here makes the emitter edge-triggered on that field: a
400
+ * value equal to the last one announced for that device is suppressed.
401
+ *
402
+ * Declare this ONLY for events carrying a settled state. An event that is a **pulse** — motion,
403
+ * a doorbell press — must be omitted: consecutive pulses are identical by nature and deduping them
404
+ * would drop real detections.
405
+ *
406
+ * Edge-triggering is applied to realtime sources only. A poll still re-announces an unchanged
407
+ * state, so a missed realtime frame is re-synchronised rather than left waiting for the state to
408
+ * change again.
409
+ */
410
+ stateEvents?: {
411
+ event: string;
412
+ field: string;
413
+ }[];
414
+ /**
415
+ * Escape hatch for inbound signals a flat {@link EventMapping} table can't express — e.g. parsing
416
+ * a binary P2P frame (the pan-tilt position stream). Return the semantic {@link CapabilityEvent}
417
+ * or `null`. Simple capabilities use only `events` and omit this.
418
+ */
419
+ decodeEvent?(signal: InboundSignal): CapabilityEvent | null;
420
+ /**
421
+ * Recover device STATE from an inbound signal, as wire ids in this device's own param namespace —
422
+ * the input to `Device.applyParams`, so a realtime-only line's values reach the same
423
+ * {@link CapabilityModule.members} getters a pollable cloud param would.
424
+ *
425
+ * Separate from {@link CapabilityModule.decodeEvent} because the two answer different questions: a
426
+ * report that repeats the current state carries no event worth emitting but must still refresh the
427
+ * readable state. A module that decodes both shares one private parser between the hooks.
428
+ */
429
+ decodeState?(signal: InboundSignal): DecodedState | null;
430
+ /**
431
+ * Commands to send once this device's realtime channel is up — e.g. a state-snapshot request, for a
432
+ * line whose state is pushed on change with no periodic heartbeat, so the typed reads are populated
433
+ * at connect instead of only after the first write. Best-effort: a failure is reported, never fatal.
434
+ */
435
+ realtimeInit?(ctx: CommandContext): Command[];
436
+ /**
437
+ * Resolve a semantic action into a transport-neutral {@link Command} for THIS device, or
438
+ * `undefined` if this module doesn't handle `(action)`. `action` is a capability-local verb
439
+ * (e.g. `"on"`, `"off"`, `"setBrightness"`, `"rotate"`), NOT a param id. Uses `ctx` to
440
+ * pick the right variant. This is where per-device variance lives — once, in the module.
441
+ */
442
+ buildCommand?(action: string, value: boolean | number | string, ctx: CommandContext): Command | undefined;
443
+ /**
444
+ * The object of bound action methods exposed on a device that HAS this capability
445
+ * (e.g. `device.light()` → `{on, off, setBrightness}`). Control actions resolve a
446
+ * {@link Command} and emit it through `sink`; media actions (snapshot/live/record) delegate to
447
+ * the optional `media` provider (absent on a model object not bound to a live client).
448
+ *
449
+ * Every injected provider here names a **technical job**, never the capability that happens to be
450
+ * its first caller: `media` is shared by every media-capable device, and `ff09Settings` is named for
451
+ * the frame family it reads, so any device driven by that frame can use it. That is the rule, not a
452
+ * convention — a provider that cannot be named without saying "lock" or "vacuum" is a provider split
453
+ * in the wrong place.
454
+ *
455
+ * `ff09Settings` exists as its own boundary because `GET_SETTINGS` is a request/reply query
456
+ * (decrypt + parse + pick P2P-vs-MQTT), and neither `CommandSink` (write-only, `Promise<void>`) nor
457
+ * `MediaProvider.p2pQuery` (P2P-only, raw passthrough, no decrypt) fits it. A further `ff09` setting
458
+ * needing a live read is one more method on {@link Ff09SettingsReader}; a genuinely different wire
459
+ * family is its own provider, not another parameter bolted on next to this one.
460
+ */
461
+ actions?(deps: MemberDeps): CapabilityActions;
462
+ /**
463
+ * The capability's surface, one entry per feature — the schema, the getters, the setters, the intent
464
+ * routes and the descriptions all derived from it. See `./members.ts`.
465
+ *
466
+ * A feature is spelled ONCE: {@link properties} is `propertiesOf(X_MEMBERS)`, never a second list.
467
+ *
468
+ * Absent only on a capability with no surface to bind: pure detection (`video`, `snapshot`), or one
469
+ * whose object is a projection rather than device params (`info`).
470
+ */
471
+ members?: Members;
472
+ }
473
+ /**
474
+ * Thrown when a control is requested on a device that doesn't support it — instead of silently
475
+ * sending a command into the void (the eufy P2P write path is fire-and-forget, so an unsupported
476
+ * command otherwise looks like success). Raised by `setProperty` / action paths when no capability
477
+ * module produces a command for the (device, action).
478
+ */
479
+ export declare class CapabilityNotSupportedError extends Error {
480
+ readonly sn: string;
481
+ readonly action: string;
482
+ constructor(sn: string, action: string);
483
+ }
484
+ export {};