@mega-yfue/eufy-sdk 0.2.0-beta.2 → 0.2.0-beta.21

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 (52) hide show
  1. package/dist/client/eufy-mega.d.ts +16 -11
  2. package/dist/client/types.d.ts +6 -2
  3. package/dist/core/contracts.d.ts +57 -27
  4. package/dist/core/crypto.d.ts +10 -0
  5. package/dist/core/index.d.ts +1 -0
  6. package/dist/core/logger.d.ts +5 -3
  7. package/dist/core/solix-types.d.ts +115 -0
  8. package/dist/core/store.d.ts +38 -10
  9. package/dist/index.js +2713 -452
  10. package/dist/index.js.map +4 -4
  11. package/dist/model/capabilities/access.d.ts +22 -3
  12. package/dist/model/capabilities/arming.d.ts +33 -27
  13. package/dist/model/capabilities/battery.d.ts +32 -4
  14. package/dist/model/capabilities/contact.d.ts +4 -0
  15. package/dist/model/capabilities/display.d.ts +85 -0
  16. package/dist/model/capabilities/index.d.ts +18 -3
  17. package/dist/model/capabilities/ptz.d.ts +6 -2
  18. package/dist/model/capabilities/solix.d.ts +173 -0
  19. package/dist/model/capabilities/types.d.ts +21 -5
  20. package/dist/model/capabilities/vacuum-clean.d.ts +74 -25
  21. package/dist/model/device.d.ts +15 -0
  22. package/dist/model/index.d.ts +5 -0
  23. package/dist/model/param-dictionary.d.ts +24 -0
  24. package/dist/model/param-namespace.d.ts +1 -1
  25. package/dist/model/solix-catalog.d.ts +25 -0
  26. package/dist/model/solix-device.d.ts +136 -0
  27. package/dist/model/solix-family.d.ts +31 -0
  28. package/dist/model/solix-site.d.ts +70 -0
  29. package/dist/model/types.d.ts +5 -5
  30. package/dist/transport/ff09.d.ts +7 -0
  31. package/dist/transport/http/decodeImageV2.d.ts +8 -14
  32. package/dist/transport/http/index.d.ts +1 -0
  33. package/dist/transport/http/jpeg-scan.d.ts +59 -0
  34. package/dist/transport/http/media-download.d.ts +3 -0
  35. package/dist/transport/http/mega-client.d.ts +89 -9
  36. package/dist/transport/http/solix-client.d.ts +269 -0
  37. package/dist/transport/http/solix-constants.d.ts +56 -0
  38. package/dist/transport/media-failure.d.ts +48 -0
  39. package/dist/transport/mqtt/index.d.ts +2 -0
  40. package/dist/transport/mqtt/secure-mqtt.d.ts +14 -1
  41. package/dist/transport/mqtt/solix-mqtt.d.ts +321 -0
  42. package/dist/transport/mqtt/topics.d.ts +30 -0
  43. package/dist/transport/p2p/command-router.d.ts +152 -15
  44. package/dist/transport/p2p/index.d.ts +1 -0
  45. package/dist/transport/p2p/live-stream.d.ts +5 -4
  46. package/dist/transport/p2p/live-trace.d.ts +118 -6
  47. package/dist/transport/p2p/media.d.ts +11 -0
  48. package/dist/transport/p2p/p2p-session.d.ts +13 -0
  49. package/dist/transport/p2p/session-manager.d.ts +57 -23
  50. package/dist/transport/p2p/shared-live-source.d.ts +10 -1
  51. package/dist/transport/stored-image-cache.d.ts +7 -1
  52. package/package.json +3 -2
@@ -192,35 +192,27 @@ export declare const ModeCtrlMethod: {
192
192
  readonly STOP_SMART_FOLLOW: 18;
193
193
  readonly START_GLOBAL_CRUISE: 20;
194
194
  };
195
- /**
196
- * The methods that carry a `Param` oneof — room, zone, goto, schedule, cruise and scene cleans.
197
- *
198
- * Deliberately absent from {@link ModeCtrlMethod}. Each needs an argument the caller has to supply and
199
- * this SDK cannot yet answer: a room or zone id comes from map data, which is not decodable here, and a
200
- * coordinate is signed centimetres in a frame no capture has pinned. Listing their numbers beside the
201
- * parameterless ones would invite a caller to send one with an empty payload, which is a valid frame
202
- * meaning something nobody intended.
203
- */
204
- /**
205
- * Encode a `ModeCtrlRequest` protobuf (DP 152) as a DP value: `varint(bodyLen) ++ {method:1, seq:2}`.
206
- *
207
- * Built on {@link RawDpWriter} rather than hand-rolled bytes. The frame is unchanged and the existing
208
- * byte-level test is what proves it — that test was written against a live T2351 capture, so it holds
209
- * the writer to the wire rather than to this function's own idea of the wire.
210
- *
211
- * Method 0 (START_AUTO_CLEAN) is omitted rather than written as an explicit zero, per the proto3
212
- * default-field rule and confirmed on that same capture. The writer deliberately does not apply that
213
- * rule itself: whether an explicit zero and an absent field mean the same thing is the
214
- * message's business, not the encoder's.
215
- * @internal
216
- */
217
195
  /**
218
196
  * The area-selecting `ModeCtrlRequest` methods, and the `Param` field each one's payload rides in.
219
197
  *
220
198
  * Kept apart from {@link ModeCtrlMethod} because these are a different kind of thing: a parameterless
221
199
  * verb is complete on its own, whereas each of these is meaningless without an argument the caller has
222
200
  * to supply. Sending one with an empty payload is a well-formed frame that means something nobody
223
- * intended, which is exactly why the numbers do not sit beside the others.
201
+ * intended, which is exactly why the numbers do not sit beside the others. Each verb built on one takes
202
+ * its argument in the signature: {@link VACUUM_CLEAN_MEMBERS.startScene},
203
+ * {@link VACUUM_CLEAN_MEMBERS.cleanRooms} and {@link VACUUM_CLEAN_MEMBERS.cleanZones}.
204
+ *
205
+ * The outer frame these ride in is byte-verified on a live T2351, and `SCENE`, `SELECT_ROOMS` and
206
+ * `SELECT_ZONES` have each since been RUN on a T2351 and did what they name — so their numbers rest on
207
+ * observed behaviour rather than on the vendor's definition alone.
208
+ *
209
+ * That distinction is the whole point of checking, and this is the one place it is argued: an AIoT
210
+ * data-point write is fire-and-forget, so a wrong number would be a different command arriving and
211
+ * looking exactly like success, which no frame check could catch. Watching the number is the only thing
212
+ * that rules it out.
213
+ *
214
+ * `GOTO` carries no encoder because a goto point is a coordinate no read on this SDK supplies, where a
215
+ * scene id and a map id both arrive on DP 180.
224
216
  */
225
217
  export declare const ModeCtrlParamMethod: {
226
218
  /** `START_SELECT_ROOMS_CLEAN` — clean the named rooms of a named map. */
@@ -267,7 +259,7 @@ export interface VacuumZoneTarget {
267
259
  * `mapId` is required and has no default, deliberately. The obvious shortcut is to assume the map a
268
260
  * single-floor home would have; on a two-floor home that silently sends the robot's ids against the
269
261
  * wrong floor's map. A caller that cannot name the map cannot safely make this call, and saying so is
270
- * better than picking for them.
262
+ * better than picking for them. {@link VACUUM_CLEAN_MEMBERS.cleanRooms} dispatches this.
271
263
  * @internal
272
264
  */
273
265
  export declare function encodeSelectRoomsClean(mapId: number, rooms: readonly VacuumRoomTarget[], cleanTimes?: number): string;
@@ -276,8 +268,25 @@ export declare function encodeSelectRoomsClean(mapId: number, rooms: readonly Va
276
268
  * @internal
277
269
  */
278
270
  export declare function encodeSelectZonesClean(mapId: number, zones: readonly VacuumZoneTarget[]): string;
279
- /** Build a scene clean, which needs only the scene's own id. @internal */
271
+ /**
272
+ * Build a scene clean, which needs only the scene's own id — `VacuumScene.id`, as DP 180 reports it.
273
+ * {@link VACUUM_CLEAN_MEMBERS.startScene} dispatches this.
274
+ * @internal
275
+ */
280
276
  export declare function encodeSceneClean(sceneId: number): string;
277
+ /**
278
+ * Encode a `ModeCtrlRequest` protobuf (DP 152) as a DP value: `varint(bodyLen) ++ {method:1, seq:2}`.
279
+ *
280
+ * Built on {@link RawDpWriter} rather than hand-rolled bytes. The frame is unchanged and the existing
281
+ * byte-level test is what proves it — that test was written against a live T2351 capture, so it holds
282
+ * the writer to the wire rather than to this function's own idea of the wire.
283
+ *
284
+ * Method 0 (START_AUTO_CLEAN) is omitted rather than written as an explicit zero, per the proto3
285
+ * default-field rule and confirmed on that same capture. The writer deliberately does not apply that
286
+ * rule itself: whether an explicit zero and an absent field mean the same thing is the
287
+ * message's business, not the encoder's.
288
+ * @internal
289
+ */
281
290
  export declare function encodeModeCtrl(method: number, seq: number): string;
282
291
  /**
283
292
  * Every value {@link VacuumActivity} can take, as data — the read's declared domain, so the schema a
@@ -1941,6 +1950,46 @@ export declare const VACUUM_CLEAN_MEMBERS: {
1941
1950
  readonly pauseCleaning: import("./members.js").MethodMember<() => Promise<void>> & {
1942
1951
  available: (ctx: import("./types.js").CommandContext) => boolean;
1943
1952
  };
1953
+ /**
1954
+ * Run a saved cleaning scene by its id (ModeCtrlRequest method 24 over DP 152).
1955
+ *
1956
+ * The id is the device's own, as {@link VACUUM_CLEAN_MEMBERS.scenes} reports it — `VacuumScene.id`
1957
+ * off the `SceneResponse` on DP 180. A scene the device reports invalid stays reportable and running
1958
+ * it is still a well-formed request; `VacuumScene.invalidReason` says why the device will refuse.
1959
+ *
1960
+ * Frame shape is byte-proven against the shared outer `ModeCtrlRequest`, and method 24 has been
1961
+ * WATCHED: run on a T2351, it started the named scene.
1962
+ */
1963
+ readonly startScene: import("./members.js").MethodMember<(sceneId: number) => Promise<void>> & {
1964
+ available: (ctx: import("./types.js").CommandContext) => boolean;
1965
+ };
1966
+ /**
1967
+ * Clean the named rooms of a named map (ModeCtrlRequest method 1 over DP 152).
1968
+ *
1969
+ * `mapId` has no default and that is deliberate: room ids are per map, so assuming the map a
1970
+ * single-floor home would have sends a two-floor home's ids against the wrong floor. `SceneInfo.mapid`
1971
+ * on DP 180 and a scheduled rooms-clean's `map_id` are the two real map ids the device reports.
1972
+ *
1973
+ * `cleanTimes` is how many passes to make over the set; rooms with no `order` are visited in the
1974
+ * order given.
1975
+ *
1976
+ * Frame shape is byte-proven against the shared outer `ModeCtrlRequest`, and method 1 has been
1977
+ * WATCHED: run on a T2351, it cleaned the rooms named.
1978
+ */
1979
+ readonly cleanRooms: import("./members.js").MethodMember<(mapId: number, rooms: readonly VacuumRoomTarget[], cleanTimes?: number) => Promise<void>> & {
1980
+ available: (ctx: import("./types.js").CommandContext) => boolean;
1981
+ };
1982
+ /**
1983
+ * Clean the given rectangles of a named map (ModeCtrlRequest method 2 over DP 152).
1984
+ *
1985
+ * Corners are SIGNED centimetres in the map's own frame, whose origin sits wherever the robot first
1986
+ * mapped from — negative coordinates are ordinary and are ZigZag-encoded, not written as plain
1987
+ * varints. Same `mapId` reasoning as {@link VACUUM_CLEAN_MEMBERS.cleanRooms}, and the same evidence:
1988
+ * method 2 was run on a T2351 and cleaned the rectangles given.
1989
+ */
1990
+ readonly cleanZones: import("./members.js").MethodMember<(mapId: number, zones: readonly VacuumZoneTarget[]) => Promise<void>> & {
1991
+ available: (ctx: import("./types.js").CommandContext) => boolean;
1992
+ };
1944
1993
  };
1945
1994
  /** `vacuum_clean` — core RoboVac scalar state + decoded activity: power, activity, volume, battery. */
1946
1995
  export declare const VACUUM_CLEAN: CapabilityModule;
@@ -62,6 +62,11 @@ export declare class Device {
62
62
  * different wire ids across device families still resolves to one named value.
63
63
  */
64
64
  private specByParam;
65
+ /**
66
+ * Params a resolved capability declares that this device's schema does not carry — withheld by a
67
+ * member's gate, so not named from the param dictionary either. See {@link Device.applyParams}.
68
+ */
69
+ private withheld;
65
70
  /** Which param namespace this device's ids live in (clean DPs vs security P2P). */
66
71
  private namespace;
67
72
  /** The record's `device_name` as stated, before the {@link modelName} fallback is applied. */
@@ -201,6 +206,16 @@ export declare class Device {
201
206
  * Apply a raw param map (cloud record or P2P notification). Known params update their named
202
207
  * property; unrecognised params are retained as `unknown_<paramType>` so nothing is lost.
203
208
  *
209
+ * Naming precedence: this device's own `PropertySpec` (curated), then the param dictionary for its
210
+ * namespace, then the `unknown_<paramType>` passthrough. The dictionary def is consulted even where a
211
+ * spec exists, because `encoding` lives there.
212
+ *
213
+ * A param a resolved capability's gate WITHHELD takes the passthrough instead of its dictionary name.
214
+ * The gate decided the read does not describe this device, the dictionary names it what the member
215
+ * would have, and republishing it there hands a caller a reading indistinguishable from one the device
216
+ * really answered. A capability that never resolved withholds nothing: a param arriving before its
217
+ * capability is still the device's own, and keeps its dictionary name.
218
+ *
204
219
  * @param params param_type → raw value.
205
220
  * @param ts observation time (epoch ms); defaults to `Date.now()`.
206
221
  * @returns the list of property names whose value changed.
@@ -30,3 +30,8 @@ export * from "./capabilities/index.js";
30
30
  */
31
31
  export type { FamilyContext } from "./device-family.js";
32
32
  export { CusPushEvent, CusPushAlarmType, CusPushMode, DoorbellPushEvent, IndoorPushEvent, HB3PairedDevicePushEvent, LockPushEvent, SmartDropPushEvent, NotificationStyle, detectionName, } from "./push-events.js";
33
+ export { SolixDevice, discoverSolixDevices, solarbankSceneReadings, type SolixDeviceReader, type SolixDeviceRecord, type SolixIdentity, type SolixConnectivity, type SolixEnergyMeter, type SolixDeviceOptions, } from "./solix-device.js";
34
+ export { SOLIX_ENERGY_METER_MEMBERS, type SolixCapability, type SolixEnergyMeterReads } from "./capabilities/solix.js";
35
+ export { buildModelIndex, type SolixProduct, type SolixProductCategory } from "./solix-catalog.js";
36
+ export { solixProductFamily, isSolixPowerStation, isSolixSolarbank, isSolixSmartMeter, type SolixProductFamily, type SolixFamilyInput, } from "./solix-family.js";
37
+ export { SolixSite, discoverSolixSites, type SolixSiteReader, type SolixSiteOptions } from "./solix-site.js";
@@ -26,3 +26,27 @@ export interface ParamDef {
26
26
  export declare const SECURITY_PARAMS: Record<number, ParamDef>;
27
27
  /** RoboVac Tuya DP space (ids ~150-180), from get_product_data_point. */
28
28
  export declare const CLEAN_PARAMS: Record<number, ParamDef>;
29
+ /**
30
+ * eufy Smart Display (T87Ax) param space — ids 8001-8006.
31
+ *
32
+ * Its own table rather than a corner of {@link SECURITY_PARAMS}: nothing in the 8000s carries a security
33
+ * meaning, so reading these ids there would decode a future security param assigned in this range as
34
+ * whatever it means on a camera.
35
+ *
36
+ * Every id here was reported by a live T87A0 (captured 2026-09-04). That the device SENT an id is what
37
+ * earns it a place in this table; the provenance label beside each one rates something narrower — how far
38
+ * its NAME is trusted. `modelName` and `modelCode` are `mega`, their values matching what the cloud
39
+ * record already carried. `battery` is `verified`: the id is real and the reading consistent, but the
40
+ * name came from the maintainer's own knowledge of the hardware rather than from the cloud data-point
41
+ * list, and `"100"` fits brightness, volume or charge equally.
42
+ *
43
+ * A dictionary entry is what makes a param readable by name off `getProperties()`. `capabilities/display.ts`
44
+ * decides separately which of them reach the typed surface, and only `battery` does.
45
+ *
46
+ * **8002 (`"1"`) and 8004 (a serial-shaped string) are absent, deliberately.** Neither meaning is
47
+ * legible from one value: `1` fits any enum or flag, and a serial could be the display's own or the
48
+ * station's it is bound to. They arrive as `unknown_8002` / `unknown_8004`, which is the measure of this
49
+ * list: it holds what is unknown, not what is unknowable. What would settle them: the vendor app's own
50
+ * display settings screen, one control at a time, params diffed after each.
51
+ */
52
+ export declare const DISPLAY_PARAMS: Record<number, ParamDef>;
@@ -14,7 +14,7 @@
14
14
  import type { Codec } from "./types.js";
15
15
  import { type ParamDef } from "./param-dictionary.js";
16
16
  /** The param id spaces this SDK models. */
17
- export type ParamNamespace = "security" | "clean" | "life" | "print";
17
+ export type ParamNamespace = "security" | "clean" | "life" | "print" | "display";
18
18
  /** Look up a param def in the given namespace. */
19
19
  export declare function paramDef(ns: ParamNamespace, paramType: number): ParamDef | undefined;
20
20
  /** The param namespace a device's ids live in, from its codec, via the module-local `NAMESPACE_BY_CODEC` table. */
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Anker Solix product catalog — the vendor's pairable-product registry (categories → products),
3
+ * used to label a discovered device's model code with a marketing name + category.
4
+ *
5
+ * This is model-layer vocabulary: pure data shapes + a lookup builder, with no wire/transport
6
+ * dependency. The catalog itself is fetched from the cloud by the client layer (`SolixClient`), which
7
+ * feeds it here via {@link buildModelIndex}; keeping the shapes and the index in the model layer lets
8
+ * `SolixDevice` resolve a name/category without reaching across the capability↔transport boundary.
9
+ */
10
+ export type { SolixProduct, SolixProductCategory } from "../core/solix-types.js";
11
+ import type { SolixProductCategory } from "../core/solix-types.js";
12
+ /**
13
+ * Flatten a product catalog into a `product_code → { name, category }` lookup for labelling
14
+ * discovered devices. Every variant code in `p_codes` maps to its parent product too, so a device
15
+ * reporting a sub-model resolves to the same marketing name.
16
+ *
17
+ * The category name is trimmed here, at ingest — the live catalog returns some names with trailing
18
+ * whitespace (e.g. `"Plug-in Home Battery "`), and normalising once at the source means every consumer
19
+ * (a device's `identity().category`, `detectSolixCapabilities`, and any exact-match category test) sees
20
+ * the clean name, rather than each call site having to remember to trim.
21
+ */
22
+ export declare function buildModelIndex(categories: SolixProductCategory[]): Map<string, {
23
+ name: string;
24
+ category: string;
25
+ }>;
@@ -0,0 +1,136 @@
1
+ import { type SolixCapability, type SolixEnergyMeterReads } from "./capabilities/solix.js";
2
+ import { type SolixProductFamily } from "./solix-family.js";
3
+ import type { SolixDeviceRecord, SolixProductCategory, SolixSiteScene } from "../core/solix-types.js";
4
+ /**
5
+ * The device record shape, re-exported from the model surface. It lives in `core/solix-types` so the
6
+ * transport client can return it without crossing the transport↔model line.
7
+ */
8
+ export type { SolixDeviceRecord } from "../core/solix-types.js";
9
+ export interface SolixIdentity {
10
+ serial: string;
11
+ productCode: string;
12
+ /** Friendly name — the catalog marketing name if resolvable, else the record's alias/name. */
13
+ name: string;
14
+ /** Anker catalog category (e.g. "Accessory", "Portable Power Station"), if resolvable. */
15
+ category?: string;
16
+ /**
17
+ * Normalized product family — the "what kind of thing is this" answer (power station, Solarbank,
18
+ * smart meter, …), decorrelated from the marketing `category` string. See {@link SolixDevice.family}.
19
+ */
20
+ family: SolixProductFamily;
21
+ }
22
+ export interface SolixConnectivity {
23
+ online: boolean;
24
+ rssi?: number;
25
+ ssid?: string;
26
+ }
27
+ /**
28
+ * The `dev.energyMeter()` handle: the members-derived reads ({@link SolixEnergyMeterReads} —
29
+ * `meterVoltageL1` is present only once a frame carrying its tag has landed). Every not-yet-named meter
30
+ * quantity is read from {@link SolixDevice.telemetry} under its `channel_<hex tag>` key instead, which a
31
+ * static members table cannot enumerate.
32
+ */
33
+ export type SolixEnergyMeter = SolixEnergyMeterReads;
34
+ /** Options for {@link SolixDevice}. */
35
+ export interface SolixDeviceOptions {
36
+ /** Catalog categories (from `SolixClient.getProductCatalog()`) — used to resolve name + category. */
37
+ catalog?: SolixProductCategory[];
38
+ }
39
+ /**
40
+ * A discovered Solix device with resolved category + capabilities. Feed live telemetry with
41
+ * {@link applyReading} (from {@link SolixMqtt}'s `reading` events) to populate value accessors.
42
+ */
43
+ export declare class SolixDevice {
44
+ readonly serial: string;
45
+ readonly productCode: string;
46
+ readonly record: SolixDeviceRecord;
47
+ private readonly caps;
48
+ private readonly identity_;
49
+ private values;
50
+ constructor(record: SolixDeviceRecord, opts?: SolixDeviceOptions);
51
+ /**
52
+ * The device's {@link SolixProductFamily} — the classification a caller branches on to SORT devices
53
+ * (a site's power stations vs its meters), the Solix analogue of eufy's `isHomeBase()`. Distinct from
54
+ * {@link has}, which answers what the device can DO; family answers what KIND of device it is.
55
+ */
56
+ get family(): SolixProductFamily;
57
+ /** All capabilities this device carries. */
58
+ get capabilities(): SolixCapability[];
59
+ /** Whether the device carries a capability — the only correct way to branch on behaviour. */
60
+ has(capability: SolixCapability): boolean;
61
+ /**
62
+ * Merge a live telemetry reading (a `SolixMqtt` `reading` event) so accessors reflect it. Takes the
63
+ * WHOLE reading, not just its values, and drops one addressed to a different device: the documented
64
+ * wiring is `mqtt.on("reading", r => device.applyReading(r))`, and one MQTT stream carries every
65
+ * watched meter on the account — so without this filter two meters would cross-feed each other's floats.
66
+ * A reading with no `deviceSn` (a hand-built one) is accepted as-is.
67
+ */
68
+ applyReading(reading: {
69
+ deviceSn?: string;
70
+ values: Record<string, number>;
71
+ }): void;
72
+ /** All decoded float telemetry channels from the latest applied reading (raw, `channel_<tag>` keys). */
73
+ telemetry(): Record<string, number>;
74
+ identity(): SolixIdentity;
75
+ firmware(): {
76
+ version: string;
77
+ } | undefined;
78
+ connectivity(): SolixConnectivity | undefined;
79
+ /**
80
+ * The members-derived `energyMeter` reads, or `undefined` when the device has no meter. Each getter is
81
+ * installed only for a tag this device has actually reported, and reads `this.values` LIVE — a handle
82
+ * held across an {@link applyReading} reflects the newer values. Getter INSTALLATION is fixed at the
83
+ * time of this call, so re-call it to pick up a tag first seen since.
84
+ */
85
+ energyMeter(): SolixEnergyMeter | undefined;
86
+ /**
87
+ * The {@link MemberDeps} the members engine needs: a read closure over the live values store, and the
88
+ * evidence gate (`ctx.paramIds`) rebuilt from the ff09 tags this device has reported. No `codec` — the
89
+ * codecs are eufy transport families and a Solix device belongs to none of them. The sink is a no-op:
90
+ * Solix telemetry is read-only, no member here dispatches a command.
91
+ */
92
+ private meterDeps;
93
+ /** The ff09 tags this device has reported, derived from the decoder's `channel_<hex>` keys. */
94
+ private seenTags;
95
+ }
96
+ /**
97
+ * The minimum a client must offer to be discovered against — the two wire reads {@link discoverSolixDevices}
98
+ * composes. Typed STRUCTURALLY (not as `SolixClient`) so the model layer never imports the transport
99
+ * client: the hard `transport ⊥ model` rule forbids it, and a structural shape needs no import.
100
+ */
101
+ export interface SolixDeviceReader {
102
+ getDevices(): Promise<SolixDeviceRecord[]>;
103
+ getProductCatalog(): Promise<SolixProductCategory[]>;
104
+ }
105
+ /**
106
+ * Resolve the product catalog for a discovery: a caller-supplied `opts.catalog` short-circuits the wire
107
+ * read, and a failed `getProductCatalog()` degrades to no categories (names/families just go unresolved,
108
+ * never a thrown discovery). Shared by {@link discoverSolixDevices} and `discoverSolixSites` so that
109
+ * "a failed catalog is non-fatal" decision lives in exactly one place.
110
+ */
111
+ export declare function resolveSolixCatalog(client: SolixDeviceReader, opts: {
112
+ catalog?: SolixProductCategory[];
113
+ }): Promise<SolixProductCategory[]>;
114
+ /**
115
+ * Discover an account's Solix devices as capability-driven {@link SolixDevice} objects — the wire+model
116
+ * composition (a transport read + the product catalog) that used to be `SolixClient.discoverDevices()`.
117
+ * It lives in the model layer because it builds `SolixDevice`; the wire client (now `transport/http`)
118
+ * cannot, and passing it structurally keeps the layers decorrelated. Feed live telemetry to each result
119
+ * via `SolixDevice.applyReading`.
120
+ */
121
+ export declare function discoverSolixDevices(client: SolixDeviceReader, opts?: {
122
+ catalog?: SolixProductCategory[];
123
+ }): Promise<SolixDevice[]>;
124
+ /**
125
+ * Reduce a site "scene" snapshot to per-device telemetry readings — the BACKSTOP counterpart to the
126
+ * realtime `ff09` decode. It emits only the fields the scene reliably carries that the fast MQTT frame
127
+ * does NOT: `batteryTemperature` (the realtime frame's BMS blob is empty, so `solixReadings` withholds
128
+ * it) and `batterySoc` (a cross-check/seed for the `0xa3` SOC). Each reading is shaped exactly like a
129
+ * `SolixMqtt` `reading` event — `{ deviceSn, values }` — so a caller can feed it straight into
130
+ * {@link SolixDevice.applyReading} and broadcast it on the same path as a live frame. Entries with no
131
+ * usable value are dropped, so a poll during a gap emits nothing rather than clobbering live values.
132
+ */
133
+ export declare function solarbankSceneReadings(scene: SolixSiteScene): {
134
+ deviceSn: string;
135
+ values: Record<string, number>;
136
+ }[];
@@ -0,0 +1,31 @@
1
+ /**
2
+ * A Solix device's product family — the normalized "what kind of thing is this" answer, decorrelated
3
+ * from the Anker catalog's marketing category strings. `unknown` when neither the product code nor the
4
+ * category identifies a family (never a guess).
5
+ */
6
+ export type SolixProductFamily = "powerStation" | "solarbank" | "smartMeter" | "powerBank" | "cooler" | "evCharger" | "charger" | "unknown";
7
+ /** The pure evidence a family decision reads: the product code, and the catalog category when resolved. */
8
+ export interface SolixFamilyInput {
9
+ /** SKU / model code, e.g. `A1782`, `AE103`, `AE1X0`. */
10
+ product_code: string;
11
+ /** Anker catalog category, when a catalog resolved one (e.g. `"Portable Power Station"`). */
12
+ category?: string;
13
+ }
14
+ /** Grid-tie Solarbank / plug-in home battery family (by product code, catalog-independent). */
15
+ export declare const isSolixSolarbank: (input: SolixFamilyInput) => boolean;
16
+ /** Smart energy meter / grid-CT family (by product code, catalog-independent). */
17
+ export declare const isSolixSmartMeter: (input: SolixFamilyInput) => boolean;
18
+ /** Portable Power Station (SOLIX F-series) family — resolved from the catalog category. */
19
+ export declare const isSolixPowerStation: (input: SolixFamilyInput) => boolean;
20
+ /**
21
+ * Resolve a Solix device's {@link SolixProductFamily} by consulting the family predicates first (they are
22
+ * catalog-independent, so a device classifies even before a catalog is fetched), then the catalog
23
+ * category for the families that have no prefix set yet. Returns `"unknown"` when neither identifies one —
24
+ * never a guess, mirroring `device-family.ts` returning `false` on an unknown device type.
25
+ *
26
+ * The meter is checked before the Solarbank: both can present a battery-ish catalog category, but the
27
+ * meter's `AE1X0` prefix is unambiguous and a meter is never a Solarbank. The Solarbank case reuses
28
+ * {@link isSolixSolarbank} (prefix OR the home-battery category) rather than re-testing the prefix here,
29
+ * so the two never drift.
30
+ */
31
+ export declare function solixProductFamily(input: SolixFamilyInput): SolixProductFamily;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * A capability-driven model for an Anker Solix **site** — the account's home energy system, the "My
3
+ * Home" the app shows. A site is the Solix analogue of a eufy HomeBase in its GROUPING role: it is the
4
+ * system that member devices belong to, so it is what a caller reaches for to ask "what's in this
5
+ * system" and to sort the members by family ({@link SolixSite.powerStations} / {@link solarbanks} /
6
+ * {@link smartMeters}) or by capability ({@link SolixSite.withCapability}).
7
+ *
8
+ * A site is a *grouping*, not a device: it carries no telemetry of its own. Its members are full
9
+ * {@link SolixDevice} models (each with its own capabilities + live telemetry), resolved by
10
+ * {@link discoverSolixSites} from the site record's member list joined to the account's device records.
11
+ * The realtime SYSTEM aggregate the app draws (instantaneous battery SoC + solar + grid flow) is not
12
+ * modelled here: it has no confirmed, stable read surface for the current device generation, and this
13
+ * layer does not fabricate a getter for a value it cannot ground — a caller reads each member device's
14
+ * telemetry instead.
15
+ */
16
+ import { SolixDevice, type SolixDeviceReader } from "./solix-device.js";
17
+ import type { SolixProductFamily } from "./solix-family.js";
18
+ import type { SolixCapability } from "./capabilities/solix.js";
19
+ import type { SolixProductCategory, SolixSiteRecord } from "../core/solix-types.js";
20
+ /**
21
+ * The minimum a client must offer to discover an account's SITES against — {@link SolixDeviceReader}
22
+ * (the device + catalog reads {@link discoverSolixDevices} already composes) plus the site read.
23
+ * Structural (not `SolixClient`) so the model layer never imports the transport client: the hard
24
+ * `transport ⊥ model` rule forbids it, and a structural shape needs no import.
25
+ */
26
+ export interface SolixSiteReader extends SolixDeviceReader {
27
+ getSites(): Promise<SolixSiteRecord[]>;
28
+ }
29
+ /** Options for {@link SolixSite} / {@link discoverSolixSites} — the same catalog the device model takes. */
30
+ export interface SolixSiteOptions {
31
+ catalog?: SolixProductCategory[];
32
+ }
33
+ /**
34
+ * A discovered Solix site with its member devices resolved to {@link SolixDevice} models. Build one
35
+ * directly from a record + members, or discover an account's sites with {@link discoverSolixSites}.
36
+ */
37
+ export declare class SolixSite {
38
+ readonly id: string;
39
+ /** Friendly site name (e.g. "My Home"), or the site id when the record carries none. */
40
+ readonly name: string;
41
+ /** Anker's site-type discriminator (e.g. 20 for a Solarbank-anchored home system), when present. */
42
+ readonly powerSiteType?: number;
43
+ readonly record: SolixSiteRecord;
44
+ readonly devices: SolixDevice[];
45
+ constructor(record: SolixSiteRecord, devices: SolixDevice[]);
46
+ /** The member device with this serial, if it belongs to the site. */
47
+ device(serial: string): SolixDevice | undefined;
48
+ /** Member devices of a given product {@link SolixProductFamily} — the grouping accessor. */
49
+ withFamily(family: SolixProductFamily): SolixDevice[];
50
+ /** Member devices that carry a given capability (e.g. every `battery` in the system). */
51
+ withCapability(capability: SolixCapability): SolixDevice[];
52
+ /** The Solarbank / plug-in home-battery members of the system. */
53
+ solarbanks(): SolixDevice[];
54
+ /** The smart-meter (grid-CT) members of the system. */
55
+ smartMeters(): SolixDevice[];
56
+ /** The portable power-station members of the system. */
57
+ powerStations(): SolixDevice[];
58
+ }
59
+ /**
60
+ * Discover an account's Solix sites as {@link SolixSite} groupings of capability-driven
61
+ * {@link SolixDevice} members — the site analogue of {@link discoverSolixDevices}. Composes three wire
62
+ * reads (the sites, the account's device records, the product catalog) entirely model-side, so the
63
+ * transport client is passed structurally and the layers stay decorrelated.
64
+ *
65
+ * A site member is resolved to its FULL device record from `getDevices()` when the account lists one
66
+ * (so the member carries firmware/connectivity + telemetry identity); a member the flat device list
67
+ * omits is built from the site entry alone (serial + product code + name), so the system is never
68
+ * missing a device it declares.
69
+ */
70
+ export declare function discoverSolixSites(client: SolixSiteReader, opts?: SolixSiteOptions): Promise<SolixSite[]>;
@@ -28,10 +28,10 @@
28
28
  *
29
29
  * `display` is the T87Ax Smart Display line — its own codec because its `device_type` collides with
30
30
  * the security residual range (confirmed live, 2026-09-04: it connects over secure MQTT with no
31
- * `p2p_did`, never P2P). Its param namespace and product line are nonetheless grouped into `security`
32
- * by maintainer decision, not wire evidence — see {@link namespaceForCodec}. No capability module
33
- * targets it yet: no screen/audio/assistant param has been observed, only its own small cloud-param
34
- * namespace (ids 8001-8006).
31
+ * `p2p_did`, never P2P). It owns its own param namespace (ids 8001-8006) and its own product line, so
32
+ * no other line's capability can attach to it — see `namespaceForCodec` and `CODEC_LINE`. The `display`
33
+ * capability targets it and reads the charge; no screen/audio/assistant param has been observed, so
34
+ * those are absent rather than deferred.
35
35
  */
36
36
  export type Codec = "station" | "camera" | "sensor" | "lock" | "keypad" | "vacuum" | "mower" | "light" | "printer" | "display";
37
37
  /**
@@ -39,7 +39,7 @@ export type Codec = "station" | "camera" | "sensor" | "lock" | "keypad" | "vacuu
39
39
  * it maps to a {@link CapabilityModule} that owns its property schema. Extend this union as
40
40
  * new capabilities are modelled — adding one never requires a subclass.
41
41
  */
42
- export type Capability = "video" | "snapshot" | "motion" | "person_detection" | "battery" | "light" | "ptz" | "doorbell" | "contact" | "leak" | "smoke" | "co" | "siren" | "lock" | "keypad" | "arming" | "storage" | "rtsp" | "camera" | "audio" | "vacuum_clean" | "vacuum_dock" | "suction" | "locate" | "smart_light" | "info";
42
+ export type Capability = "video" | "snapshot" | "motion" | "person_detection" | "battery" | "light" | "ptz" | "doorbell" | "contact" | "leak" | "smoke" | "co" | "siren" | "lock" | "keypad" | "arming" | "storage" | "rtsp" | "camera" | "audio" | "vacuum_clean" | "vacuum_dock" | "suction" | "locate" | "smart_light" | "display" | "info";
43
43
  /** Value type of a property. */
44
44
  export type PropertyValueType = "bool" | "number" | "string" | "enum";
45
45
  /**
@@ -397,6 +397,13 @@ export interface Ff09SettingsResponse {
397
397
  * padding past the last real field.
398
398
  */
399
399
  export declare function parseFf09SettingsResponse(plain: Buffer): Ff09SettingsResponse;
400
+ /**
401
+ * Walk a bounded `tag|len|value` TLV region into a `tag → bytes` map. Stops at a `0x00` tag (only ever
402
+ * trailing zero-padding, never a real field — real tags start at `0xa1`) and refuses a field whose
403
+ * declared length would overrun `end`, so a corrupt length can't read past the region (e.g. into a
404
+ * trailing checksum). Shared by {@link parseFf09SettingsResponse} and the Solix param decoder.
405
+ */
406
+ export declare function walkFf09Tlv(buf: Buffer, start: number, end: number): Map<number, Buffer>;
400
407
  /** Read a little-endian u16 out of a TLV field buffer (throws on a missing/short field — a caller-side bug, not a wire ambiguity). */
401
408
  export declare function readFf09U16LE(field: Buffer | undefined, name: string): number;
402
409
  /** Read a single byte out of a TLV field buffer (throws on a missing field — same convention as {@link readFf09U16LE}). */
@@ -1,19 +1,13 @@
1
- /**
2
- * Per-channel auto-contrast, a byte-exact port of PIL `ImageOps.autocontrast` (cutoff 0.5%). The lost
3
- * quant tables leave the reconstruction low-contrast ("foggy"); stretching each channel's clipped range
4
- * to full scale restores a natural-looking image. Mutates `data` (RGBA) in place.
5
- *
6
- * Parity notes vs PIL: the cutoff count is `n * cutoff // 100` (integer floor); it is trimmed off each
7
- * end by zeroing whole histogram bins until the count is spent; the range is then the first/last
8
- * non-empty bins; and the LUT truncates toward zero (`int()`), NOT rounds — rounding would shift pixels.
9
- */
10
- export declare function autoContrast(data: Uint8Array, width: number, height: number, cutoff?: number): void;
11
1
  /** True if the blob is a v2 `v2_eufysecurity:` push thumbnail. */
12
2
  export declare function isV2Image(data: Buffer): boolean;
13
3
  /**
14
- * Decode a v2 blob to a plain JPEG buffer by reconstructing its header, or null if it isn't v2 or the
15
- * plaintext scan can't be located. The search first chooses subsampling and coarse geometry, derives
16
- * the fixed MCU count, refines width by row shear, and pins the exact fill height before applying
17
- * auto-contrast and re-encoding. See the module doc for the keyless-splice rationale.
4
+ * Decode a v2 blob to a plain JPEG buffer by reconstructing its header, or null if it isn't v2, the
5
+ * plaintext scan can't be located, or no frame shape explains it.
6
+ *
7
+ * Three steps and no pixel rewrite: the frame shape is read out of the entropy scan,
8
+ * one probe decode both proves the spliced JPEG decodes and measures how far the substitute quant
9
+ * tables fall short of the camera's, and the answer is the same tail under a header carrying the
10
+ * corrected tables. See the module doc for the keyless-splice rationale and for why neither the search
11
+ * nor the correction decodes candidate frames or re-encodes the picture.
18
12
  */
19
13
  export declare function decodeImageV2(data: Buffer): Buffer | null;
@@ -3,3 +3,4 @@ export { randomPhoneModel, randomUserAgent } from "./phone-model.js";
3
3
  export * from "./decodeImageV1.js";
4
4
  export * from "./decodeImageV2.js";
5
5
  export { listLightEffects, listAiSceneRecommendations, type LightEffectSummary } from "./light-catalog.js";
6
+ export { SolixClient, type SolixClientOptions, type SolixLoginResult, type SolixSession, type SolixPersisted, type SolixSessionStore, } from "./solix-client.js";
@@ -0,0 +1,59 @@
1
+ /**
2
+ * A baseline-JPEG entropy scanner that decodes no pixels.
3
+ *
4
+ * The v2 thumbnail decoder next door has to discover a frame geometry its blob does not state, and the
5
+ * only evidence is the plaintext entropy-coded scan: how many MCUs it carries, and whether it carries
6
+ * them under a given chroma subsampling. A JPEG decoder answers that — it throws on a frame the scan
7
+ * does not fill — and charges a full set of component and output buffers for each question.
8
+ *
9
+ * That bill is fatal for a caller under a hard memory cap. Each `jpeg-js` decode churns roughly a
10
+ * megabyte of typed arrays, glibc keeps the arenas rather than handing them back, and a search asking
11
+ * the question of every candidate geometry costs tens of megabytes per thumbnail — permanently, and
12
+ * more than an embedded host gives an app in total.
13
+ *
14
+ * This module asks the same question by walking the Huffman-coded coefficients and throwing them away:
15
+ * no IDCT, no component planes, no output image. What it keeps is one number per MCU — the DC
16
+ * coefficient of its luma block(s), i.e. that block's average brightness — which is all the geometry
17
+ * search needs to tell a sheared row apart from a continuous one. Allocation is a single `Int32Array`
18
+ * of MCU count, and the walk is linear in the scan's length.
19
+ *
20
+ * Baseline sequential only (SOF0), which is what the v2 tail is: no progressive refinement, no
21
+ * arithmetic coding. Restart markers are tolerated — the scan resynchronises and resets its DC
22
+ * predictors, exactly as a decoder would.
23
+ *
24
+ * @module transport/http/jpeg-scan
25
+ */
26
+ /** What the scan carried, under one subsampling hypothesis. */
27
+ export interface EntropyScan {
28
+ /** Complete MCUs decoded before the data ran out. */
29
+ mcus: number;
30
+ /**
31
+ * Mean luma DC per MCU, in scan order — a thumbnail of the picture at MCU resolution.
32
+ *
33
+ * Quantized units (the quant tables are lost with the v2 head), so the values are a scale of their
34
+ * own. Differences between neighbours are what the geometry search reads, and those survive.
35
+ */
36
+ luma: Int32Array;
37
+ /**
38
+ * Whether the scan ended where a whole MCU ended, with nothing but the EOI marker left.
39
+ *
40
+ * The discriminator between hypotheses: a wrong one reads a block with the wrong Huffman table,
41
+ * diverges, and either dies mid-MCU or stops with data still ahead of it. Only the subsampling the
42
+ * encoder used walks the scan to its last byte on an MCU boundary.
43
+ */
44
+ complete: boolean;
45
+ }
46
+ /**
47
+ * Walk the plaintext tail's scan under one subsampling hypothesis.
48
+ *
49
+ * `extraTables` carries the DHT segments that are NOT in the tail — for a v2 thumbnail, the standard
50
+ * luma tables the reconstructed header supplies, since only the chroma ones survive in plaintext.
51
+ * Returns null when the tail is not a baseline scan at all (no SOS, missing tables).
52
+ *
53
+ * The walk stops at the first byte it cannot read as this hypothesis's next block, and the result is
54
+ * `complete` only when that happened on an MCU boundary with nothing but a marker left. The DC array
55
+ * grows as the scan turns out to be long rather than being sized from the scan's byte length: a wrong
56
+ * hypothesis dies after a handful of MCUs, and provisioning three whole-scan arrays to discover that is
57
+ * the kind of allocation this module exists to avoid.
58
+ */
59
+ export declare function scanEntropy(tail: Uint8Array, subsampling: number, extraTables: readonly Uint8Array[]): EntropyScan | null;
@@ -1,7 +1,10 @@
1
+ import type { MediaFailureReason } from "../media-failure.js";
1
2
  type ResolveHost = (hostname: string) => Promise<readonly {
2
3
  address: string;
3
4
  family: number;
4
5
  }[]>;
6
+ /** Tag a failure OUTSIDE the transfer itself — the push-image decoder refusing bytes that did arrive. @internal */
7
+ export declare function mediaFailureError(message: string, reason: MediaFailureReason, cause?: unknown): Error;
5
8
  /** Signals rejection of the active Eufy session without exposing response content. @internal */
6
9
  export declare class MediaDownloadAuthenticationError extends Error {
7
10
  }