@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.
- package/dist/client/eufy-mega.d.ts +16 -11
- package/dist/client/types.d.ts +6 -2
- package/dist/core/contracts.d.ts +57 -27
- package/dist/core/crypto.d.ts +10 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/logger.d.ts +5 -3
- package/dist/core/solix-types.d.ts +115 -0
- package/dist/core/store.d.ts +38 -10
- package/dist/index.js +2713 -452
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/access.d.ts +22 -3
- package/dist/model/capabilities/arming.d.ts +33 -27
- package/dist/model/capabilities/battery.d.ts +32 -4
- package/dist/model/capabilities/contact.d.ts +4 -0
- package/dist/model/capabilities/display.d.ts +85 -0
- package/dist/model/capabilities/index.d.ts +18 -3
- package/dist/model/capabilities/ptz.d.ts +6 -2
- package/dist/model/capabilities/solix.d.ts +173 -0
- package/dist/model/capabilities/types.d.ts +21 -5
- package/dist/model/capabilities/vacuum-clean.d.ts +74 -25
- package/dist/model/device.d.ts +15 -0
- package/dist/model/index.d.ts +5 -0
- package/dist/model/param-dictionary.d.ts +24 -0
- package/dist/model/param-namespace.d.ts +1 -1
- package/dist/model/solix-catalog.d.ts +25 -0
- package/dist/model/solix-device.d.ts +136 -0
- package/dist/model/solix-family.d.ts +31 -0
- package/dist/model/solix-site.d.ts +70 -0
- package/dist/model/types.d.ts +5 -5
- package/dist/transport/ff09.d.ts +7 -0
- package/dist/transport/http/decodeImageV2.d.ts +8 -14
- package/dist/transport/http/index.d.ts +1 -0
- package/dist/transport/http/jpeg-scan.d.ts +59 -0
- package/dist/transport/http/media-download.d.ts +3 -0
- package/dist/transport/http/mega-client.d.ts +89 -9
- package/dist/transport/http/solix-client.d.ts +269 -0
- package/dist/transport/http/solix-constants.d.ts +56 -0
- package/dist/transport/media-failure.d.ts +48 -0
- package/dist/transport/mqtt/index.d.ts +2 -0
- package/dist/transport/mqtt/secure-mqtt.d.ts +14 -1
- package/dist/transport/mqtt/solix-mqtt.d.ts +321 -0
- package/dist/transport/mqtt/topics.d.ts +30 -0
- package/dist/transport/p2p/command-router.d.ts +152 -15
- package/dist/transport/p2p/index.d.ts +1 -0
- package/dist/transport/p2p/live-stream.d.ts +5 -4
- package/dist/transport/p2p/live-trace.d.ts +118 -6
- package/dist/transport/p2p/media.d.ts +11 -0
- package/dist/transport/p2p/p2p-session.d.ts +13 -0
- package/dist/transport/p2p/session-manager.d.ts +57 -23
- package/dist/transport/p2p/shared-live-source.d.ts +10 -1
- package/dist/transport/stored-image-cache.d.ts +7 -1
- 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
|
-
/**
|
|
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;
|
package/dist/model/device.d.ts
CHANGED
|
@@ -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.
|
package/dist/model/index.d.ts
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[]>;
|
package/dist/model/types.d.ts
CHANGED
|
@@ -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).
|
|
32
|
-
*
|
|
33
|
-
* targets it
|
|
34
|
-
*
|
|
31
|
+
* `p2p_did`, never P2P). It owns its own param namespace (ids 8001-8006) and its own product line, so
|
|
32
|
+
* no other line's capability can attach to it — see `namespaceForCodec` and `CODEC_LINE`. The `display`
|
|
33
|
+
* capability targets it and reads the charge; no screen/audio/assistant param has been observed, so
|
|
34
|
+
* those are absent rather than deferred.
|
|
35
35
|
*/
|
|
36
36
|
export type Codec = "station" | "camera" | "sensor" | "lock" | "keypad" | "vacuum" | "mower" | "light" | "printer" | "display";
|
|
37
37
|
/**
|
|
@@ -39,7 +39,7 @@ export type Codec = "station" | "camera" | "sensor" | "lock" | "keypad" | "vacuu
|
|
|
39
39
|
* it maps to a {@link CapabilityModule} that owns its property schema. Extend this union as
|
|
40
40
|
* new capabilities are modelled — adding one never requires a subclass.
|
|
41
41
|
*/
|
|
42
|
-
export type Capability = "video" | "snapshot" | "motion" | "person_detection" | "battery" | "light" | "ptz" | "doorbell" | "contact" | "leak" | "smoke" | "co" | "siren" | "lock" | "keypad" | "arming" | "storage" | "rtsp" | "camera" | "audio" | "vacuum_clean" | "vacuum_dock" | "suction" | "locate" | "smart_light" | "info";
|
|
42
|
+
export type Capability = "video" | "snapshot" | "motion" | "person_detection" | "battery" | "light" | "ptz" | "doorbell" | "contact" | "leak" | "smoke" | "co" | "siren" | "lock" | "keypad" | "arming" | "storage" | "rtsp" | "camera" | "audio" | "vacuum_clean" | "vacuum_dock" | "suction" | "locate" | "smart_light" | "display" | "info";
|
|
43
43
|
/** Value type of a property. */
|
|
44
44
|
export type PropertyValueType = "bool" | "number" | "string" | "enum";
|
|
45
45
|
/**
|
package/dist/transport/ff09.d.ts
CHANGED
|
@@ -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
|
|
15
|
-
* plaintext scan can't be located
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
}
|