@mega-yfue/eufy-sdk 0.2.0-beta.17 → 0.2.0-beta.18
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/core/solix-types.d.ts +79 -0
- package/dist/index.js +565 -29
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/solix.d.ts +12 -1
- package/dist/model/index.d.ts +3 -1
- package/dist/model/solix-catalog.d.ts +5 -0
- package/dist/model/solix-device.d.ts +35 -1
- package/dist/model/solix-family.d.ts +31 -0
- package/dist/model/solix-site.d.ts +70 -0
- package/dist/transport/http/solix-client.d.ts +109 -3
- package/dist/transport/http/solix-constants.d.ts +27 -0
- package/dist/transport/mqtt/solix-mqtt.d.ts +89 -6
- package/dist/transport/mqtt/topics.d.ts +13 -3
- package/package.json +1 -1
|
@@ -143,7 +143,18 @@ export declare const CATEGORY_CAPABILITIES: Readonly<Record<string, readonly Sol
|
|
|
143
143
|
export declare const SOLIX_METER_MODELS: readonly string[];
|
|
144
144
|
/**
|
|
145
145
|
* Product-code prefixes for the grid-tie Solarbank / home-battery family (detects `battery` +
|
|
146
|
-
* `solarInput` regardless of category
|
|
146
|
+
* `solarInput` regardless of category, so a caller that builds a device without the catalog still gets
|
|
147
|
+
* them): `A1790` = Solarbank E1600 gen-1, `A17C*` = Solarbank 2 / 3, `AE10*` = Solarbank 4 E5000 Pro /
|
|
148
|
+
* SOLIX Power Dock.
|
|
149
|
+
*
|
|
150
|
+
* Grounded in the live `product_categories` catalog: `A17C0`–`A17C5` and `AE100` all list under
|
|
151
|
+
* category `"Plug-in Home Battery "`, and `AE103` by the device spec. `AE1X0`/`AE1R0` meters start
|
|
152
|
+
* `AE1X`/`AE1R`, so `AE10` does not catch them.
|
|
153
|
+
*
|
|
154
|
+
* The speculative `A17E` ("Solarbank Max AC") and `AE11` ("Solarbank Max") were dropped: neither is in
|
|
155
|
+
* the catalog, and the only `AE11x` product there — `AE113` "XE 6/8kW" — is a Residential Storage
|
|
156
|
+
* System, a different family whose telemetry is unverified, so granting it `battery`/`solarInput` would
|
|
157
|
+
* be an unevidenced false positive (exactly what this detection is otherwise careful to avoid).
|
|
147
158
|
*/
|
|
148
159
|
export declare const SOLARBANK_MODELS: readonly string[];
|
|
149
160
|
/** The minimum device shape {@link detectSolixCapabilities} reads. */
|
package/dist/model/index.d.ts
CHANGED
|
@@ -30,6 +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, type SolixDeviceReader, type SolixDeviceRecord, type SolixIdentity, type SolixConnectivity, type SolixEnergyMeter, type SolixDeviceOptions, } from "./solix-device.js";
|
|
33
|
+
export { SolixDevice, discoverSolixDevices, solarbankSceneReadings, type SolixDeviceReader, type SolixDeviceRecord, type SolixIdentity, type SolixConnectivity, type SolixEnergyMeter, type SolixDeviceOptions, } from "./solix-device.js";
|
|
34
34
|
export { SOLIX_ENERGY_METER_MEMBERS, type SolixCapability, type SolixEnergyMeterReads } from "./capabilities/solix.js";
|
|
35
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";
|
|
@@ -13,6 +13,11 @@ import type { SolixProductCategory } from "../core/solix-types.js";
|
|
|
13
13
|
* Flatten a product catalog into a `product_code → { name, category }` lookup for labelling
|
|
14
14
|
* discovered devices. Every variant code in `p_codes` maps to its parent product too, so a device
|
|
15
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.
|
|
16
21
|
*/
|
|
17
22
|
export declare function buildModelIndex(categories: SolixProductCategory[]): Map<string, {
|
|
18
23
|
name: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type SolixCapability, type SolixEnergyMeterReads } from "./capabilities/solix.js";
|
|
2
|
-
import
|
|
2
|
+
import { type SolixProductFamily } from "./solix-family.js";
|
|
3
|
+
import type { SolixDeviceRecord, SolixProductCategory, SolixSiteScene } from "../core/solix-types.js";
|
|
3
4
|
/**
|
|
4
5
|
* The device record shape, re-exported from the model surface. It lives in `core/solix-types` so the
|
|
5
6
|
* transport client can return it without crossing the transport↔model line.
|
|
@@ -12,6 +13,11 @@ export interface SolixIdentity {
|
|
|
12
13
|
name: string;
|
|
13
14
|
/** Anker catalog category (e.g. "Accessory", "Portable Power Station"), if resolvable. */
|
|
14
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;
|
|
15
21
|
}
|
|
16
22
|
export interface SolixConnectivity {
|
|
17
23
|
online: boolean;
|
|
@@ -42,6 +48,12 @@ export declare class SolixDevice {
|
|
|
42
48
|
private readonly identity_;
|
|
43
49
|
private values;
|
|
44
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;
|
|
45
57
|
/** All capabilities this device carries. */
|
|
46
58
|
get capabilities(): SolixCapability[];
|
|
47
59
|
/** Whether the device carries a capability — the only correct way to branch on behaviour. */
|
|
@@ -90,6 +102,15 @@ export interface SolixDeviceReader {
|
|
|
90
102
|
getDevices(): Promise<SolixDeviceRecord[]>;
|
|
91
103
|
getProductCatalog(): Promise<SolixProductCategory[]>;
|
|
92
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[]>;
|
|
93
114
|
/**
|
|
94
115
|
* Discover an account's Solix devices as capability-driven {@link SolixDevice} objects — the wire+model
|
|
95
116
|
* composition (a transport read + the product catalog) that used to be `SolixClient.discoverDevices()`.
|
|
@@ -100,3 +121,16 @@ export interface SolixDeviceReader {
|
|
|
100
121
|
export declare function discoverSolixDevices(client: SolixDeviceReader, opts?: {
|
|
101
122
|
catalog?: SolixProductCategory[];
|
|
102
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[]>;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* those records into capability-driven `SolixDevice` models is the model layer's job — see
|
|
15
15
|
* `discoverSolixDevices()` — so the two stay decorrelated (transport never imports model).
|
|
16
16
|
*/
|
|
17
|
-
import { type SessionStore, type SolixDeviceRecord, type SolixProductCategory } from "../../core/index.js";
|
|
17
|
+
import { type SessionStore, type SolixDeviceRecord, type SolixPowerCutoffOption, type SolixProductCategory, type SolixSiteRecord, type SolixSiteScene, type SolixSocParams } from "../../core/index.js";
|
|
18
18
|
import type { SecureMqttCredentials } from "../mqtt/secure-mqtt.js";
|
|
19
19
|
/** An authenticated Solix session — the token + the derived `gtoken` + the resolved API host. */
|
|
20
20
|
export interface SolixSession {
|
|
@@ -136,11 +136,25 @@ export declare class SolixClient {
|
|
|
136
136
|
* (adopted from a store) is answered without a handshake.
|
|
137
137
|
*/
|
|
138
138
|
login(): Promise<SolixLoginResult>;
|
|
139
|
+
/**
|
|
140
|
+
* On a rejected `/passport/login` (non-zero code, so `data` is an error envelope not the encrypted
|
|
141
|
+
* payload), throw a diagnostic that names WHY the passport refused — the throttle (`26161`, "too
|
|
142
|
+
* frequent") vs a challenge it wants the client to satisfy. The passport marks a required captcha with
|
|
143
|
+
* a `captcha_id`/`item`; our headless client cannot answer one, so surfacing it distinguishes "wait
|
|
144
|
+
* out the rate-limit" from "a captcha is required — clear it in the app". No secrets are logged, only
|
|
145
|
+
* the code, message, and which challenge fields are present.
|
|
146
|
+
*/
|
|
147
|
+
private assertLoginAccepted;
|
|
139
148
|
/** Complete a `2fa` login with the code the passport sent. */
|
|
140
149
|
submitVerifyCode(code: string): Promise<SolixLoginResult>;
|
|
141
150
|
/**
|
|
142
151
|
* One authenticated PLAIN read for both GET and POST endpoints (no per-request encryption; carries
|
|
143
152
|
* the auth token + `gtoken` only). Routes through {@link send} so every read keeps the non-JSON guard.
|
|
153
|
+
*
|
|
154
|
+
* Self-heals a **displaced session**: Anker allows ~one session per account, so another login (the app,
|
|
155
|
+
* or a second client) invalidates this token and reads then fail with {@link SOLIX_TOKEN_KICKED_CODE}
|
|
156
|
+
* ("token does not exist because it was kicked out"). On that code this re-logs in once and retries, so
|
|
157
|
+
* a running client recovers on its own instead of failing every read until its session store is cleared.
|
|
144
158
|
*/
|
|
145
159
|
private authed;
|
|
146
160
|
/**
|
|
@@ -149,10 +163,27 @@ export declare class SolixClient {
|
|
|
149
163
|
* beyond `device_sn`/`product_code` is optional on the record, so a caller reads them defensively.
|
|
150
164
|
*/
|
|
151
165
|
getDevices(): Promise<SolixDeviceRecord[]>;
|
|
152
|
-
/**
|
|
153
|
-
|
|
166
|
+
/**
|
|
167
|
+
* The account's sites (systems); devices are grouped under a site. Each record carries its
|
|
168
|
+
* `site_device_list` (the member devices), which {@link discoverSolixSites} resolves into a
|
|
169
|
+
* capability-driven `SolixSite`. Asserted to {@link SolixSiteRecord} at this trust boundary — and
|
|
170
|
+
* `site_id` (the one field the model layer keys a `SolixSite` on) is validated here, so a record the
|
|
171
|
+
* cloud returns without a usable id is dropped rather than surfacing a `SolixSite` with `id ===
|
|
172
|
+
* undefined`; every other field is optional and read defensively.
|
|
173
|
+
*/
|
|
174
|
+
getSites(): Promise<SolixSiteRecord[]>;
|
|
154
175
|
/** Per-user AWS-IoT MQTT credentials (cert/key/endpoint/thing) for the real-time device plane. */
|
|
155
176
|
getUserMqttInfo(): Promise<SecureMqttCredentials>;
|
|
177
|
+
/**
|
|
178
|
+
* Read a site's "scene" snapshot — the app's dashboard read for a system, a plain authed read. Its
|
|
179
|
+
* battery detail (`solarbank_info.solarbank_list[]`) carries clean, correctly-named fields including
|
|
180
|
+
* `bat_temperature`, which the realtime `ff09` MQTT push does NOT reliably carry (the fast frame's BMS
|
|
181
|
+
* blob is empty, so the decoder withholds temperature). This is therefore a low-rate BACKSTOP for those
|
|
182
|
+
* gap fields — NOT the realtime source: live power/SOC still come from the MQTT push (which is what the
|
|
183
|
+
* app itself refreshes from every ~5 s; there is no clean-JSON scene PUSH). Verified live against the
|
|
184
|
+
* `ff09` floats — the two agree to the watt at the same instant.
|
|
185
|
+
*/
|
|
186
|
+
getSiteScene(siteId: string): Promise<SolixSiteScene>;
|
|
156
187
|
/**
|
|
157
188
|
* The pairable-product catalog (categories → products). This is Anker's product registry, not the
|
|
158
189
|
* account's devices — fetch it to label a discovered device's model code with a marketing name and
|
|
@@ -160,4 +191,79 @@ export declare class SolixClient {
|
|
|
160
191
|
* baked-in table.
|
|
161
192
|
*/
|
|
162
193
|
getProductCatalog(): Promise<SolixProductCategory[]>;
|
|
194
|
+
/**
|
|
195
|
+
* Write device attributes — a CONTROL write, e.g. the Solarbank ambient light
|
|
196
|
+
* `{ ambient_light_switch: 0 | 1 }` (0 = on, 1 = off). Unlike the plain authenticated reads, a write
|
|
197
|
+
* must be **encrypted + signed** with a freshly negotiated `algo_ecdh` key: the gateway accepts an
|
|
198
|
+
* unsigned write with `code 0` but the device never applies it. The token-bearing request also must
|
|
199
|
+
* NOT carry the device id (`openudid`), or the gateway answers `401 token error`. Both verified live
|
|
200
|
+
* on an AE103 (the LED-enable bit in the `ba` telemetry flips exactly as commanded).
|
|
201
|
+
*/
|
|
202
|
+
setDeviceAttrs(deviceSn: string, attributes: Record<string, unknown>): Promise<void>;
|
|
203
|
+
/**
|
|
204
|
+
* A CONTROL write: `algo_ecdh`-encrypted + signed, token-bearing but WITHOUT `openudid`. Every device
|
|
205
|
+
* control the account performs (set_device_attrs, set_power_cutoff, …) goes through this — the gateway
|
|
206
|
+
* accepts an unsigned/plain write with `code 0` but the device never applies it, and adding `openudid`
|
|
207
|
+
* to the token-bearing request returns `401 token error`. Both verified live on an AE103.
|
|
208
|
+
*/
|
|
209
|
+
private encryptedWrite;
|
|
210
|
+
/** Turn the Solarbank's ambient LED on/off — a confirmed `set_device_attrs` write. */
|
|
211
|
+
setAmbientLight(deviceSn: string, on: boolean): Promise<void>;
|
|
212
|
+
/**
|
|
213
|
+
* Read device attributes — a plain authenticated read (unlike the encrypted write). `attributes`
|
|
214
|
+
* names the keys to fetch (e.g. `["screen_off_time"]`); an empty list asks for the device's default
|
|
215
|
+
* set. Returns the gateway's attribute map as-is (values are device-typed — numbers, strings). Used
|
|
216
|
+
* to reflect a control's live state, e.g. the display/light off-timeout.
|
|
217
|
+
*/
|
|
218
|
+
getDeviceAttrs(deviceSn: string, attributes?: string[]): Promise<Record<string, unknown>>;
|
|
219
|
+
/**
|
|
220
|
+
* Set the Solarbank display's screen-off timeout, in SECONDS (`screen_off_time`). The app's picker
|
|
221
|
+
* offers 10/20/30 s and 1/5/30 min; the LCD backlight — and with it the ambient LED that the screen
|
|
222
|
+
* gates — turns off after this idle period. This is the raw-seconds write; the caller maps its own UI
|
|
223
|
+
* options to seconds. The "Never" (always-on) sentinel is device-defined and NOT assumed here — pass
|
|
224
|
+
* the exact integer read back from {@link getDeviceAttrs} while the device is in that mode.
|
|
225
|
+
*/
|
|
226
|
+
setScreenOffTime(deviceSn: string, seconds: number): Promise<void>;
|
|
227
|
+
/**
|
|
228
|
+
* Read the Solarbank's battery discharge-cutoff (minimum-SOC) options — a plain authed read.
|
|
229
|
+
* The gateway returns a preset list (`power_cutoff_data`): each entry is a selectable minimum
|
|
230
|
+
* state-of-charge `output_cutoff_data` (percent) with its `id` and `is_selected` flag. The caller
|
|
231
|
+
* presents these options and writes the chosen `id` back via {@link setPowerCutoff} — the values and
|
|
232
|
+
* ids come from the device, never assumed. `siteId` is optional (the device knows its own cutoff).
|
|
233
|
+
*/
|
|
234
|
+
getPowerCutoff(deviceSn: string, siteId?: string): Promise<SolixPowerCutoffOption[]>;
|
|
235
|
+
/**
|
|
236
|
+
* Select the Solarbank's battery discharge-cutoff (minimum SOC) by option id — a control write.
|
|
237
|
+
* `cutoffDataId` MUST be an `id` returned by {@link getPowerCutoff} for this device (the preset the
|
|
238
|
+
* user picked), never a raw percentage; the gateway maps the id to its cutoff percent.
|
|
239
|
+
*/
|
|
240
|
+
setPowerCutoff(deviceSn: string, cutoffDataId: number): Promise<void>;
|
|
241
|
+
/** The `param_type` under which the Solarbank's SOC-limit block lives (verified live on an AE103). */
|
|
242
|
+
private static readonly SOC_PARAM_TYPE;
|
|
243
|
+
/** `cmd` value that scopes the `site/*_site_device_param` family (from the app's request builder). */
|
|
244
|
+
private static readonly SITE_DEVICE_PARAM_CMD;
|
|
245
|
+
/**
|
|
246
|
+
* Read one of a site's "device param" blocks by `param_type` — a plain authenticated read whose
|
|
247
|
+
* `data.param_data` is itself a JSON STRING (the vendor double-encodes it). Returns the parsed inner
|
|
248
|
+
* object, or `{}` when the block is empty (the gateway answers `code 0` with an empty `param_data`
|
|
249
|
+
* for a `param_type` that does not apply to the site's hardware). The caller owns the inner shape.
|
|
250
|
+
*/
|
|
251
|
+
private getSiteDeviceParam;
|
|
252
|
+
/**
|
|
253
|
+
* Read the Solarbank's battery SOC-limit settings (`param_type "27"`) — a plain authenticated read.
|
|
254
|
+
* Returns `undefined` when the site carries no SOC block (e.g. non-Solarbank hardware). The realtime
|
|
255
|
+
* `dischargeLowerLimit` also arrives on the MQTT `b5` telemetry blob; this is the authoritative,
|
|
256
|
+
* app-synced source (and the only source for `chargeUpperLimit` / `backupReserve`). Verified live
|
|
257
|
+
* against a known AE103 setting (discharge 20 / charge 80).
|
|
258
|
+
*/
|
|
259
|
+
getSafetySocParams(siteId: string): Promise<SolixSocParams | undefined>;
|
|
260
|
+
/**
|
|
261
|
+
* Write the Solarbank's battery SOC limits — an `algo_ecdh`-encrypted + signed control write. This is
|
|
262
|
+
* **read-modify-write**: it first reads the current `param_type "27"` block and overlays only the
|
|
263
|
+
* fields the caller supplies, so changing the discharge limit alone never clobbers the charge limit,
|
|
264
|
+
* backup reserve, or calibration toggle. `changes` values are whole-percent integers. The full block
|
|
265
|
+
* (all five keys) is sent, matching the app's `SocSettingParam.toJson`. Throws if the site has no SOC
|
|
266
|
+
* block to modify. Returns the merged parameters that were written (for an immediate optimistic echo).
|
|
267
|
+
*/
|
|
268
|
+
setSafetySocParams(siteId: string, changes: Partial<SolixSocParams>): Promise<SolixSocParams>;
|
|
163
269
|
}
|
|
@@ -26,4 +26,31 @@ export declare const SOLIX_ENDPOINTS: {
|
|
|
26
26
|
readonly getUserMqttInfo: "/v1/openapi/devicemanage/get_user_mqtt_info";
|
|
27
27
|
/** GET: the pairable-product catalog (categories → products), for labelling model codes. */
|
|
28
28
|
readonly productCategories: "/power_service/v1/product_categories";
|
|
29
|
+
/** POST (encrypted+signed): write device attributes, e.g. `{ambient_light_switch: 0|1}`. */
|
|
30
|
+
readonly setDeviceAttrs: "/power_service/v1/app/device/set_device_attrs";
|
|
31
|
+
/** POST (plain authed): read device attributes, e.g. the display `screen_off_time` (seconds). */
|
|
32
|
+
readonly getDeviceAttrs: "/power_service/v1/app/device/get_device_attrs";
|
|
33
|
+
/** POST (plain authed): the battery discharge-cutoff (minimum-SOC) preset options. */
|
|
34
|
+
readonly getPowerCutoff: "/power_service/v1/app/compatible/get_power_cutoff";
|
|
35
|
+
/** POST (encrypted+signed): select the discharge-cutoff preset by `cutoff_data_id`. */
|
|
36
|
+
readonly setPowerCutoff: "/power_service/v1/app/compatible/set_power_cutoff";
|
|
37
|
+
/**
|
|
38
|
+
* POST (plain authed): the site "scene" snapshot — the same clean Solarbank/grid telemetry the app
|
|
39
|
+
* reads on load/refresh. Used as a low-rate BACKSTOP for the fields the realtime `ff09` push doesn't
|
|
40
|
+
* carry reliably (notably `bat_temperature`), NOT as the realtime source (that is the MQTT push).
|
|
41
|
+
*/
|
|
42
|
+
readonly getSiteScene: "/power_service/v2/site/platform_get_site_scene";
|
|
43
|
+
/**
|
|
44
|
+
* POST (plain authed): read a site "device param" block by `param_type`. Body is
|
|
45
|
+
* `{ site_id, param_type, cmd: 246 }`; the response's `data.param_data` is a JSON STRING the caller
|
|
46
|
+
* parses. The Solarbank's SOC-limit settings live under `param_type "27"` (charge/discharge limits,
|
|
47
|
+
* backup reserve) — verified live on an AE103 (`"18"` returns empty for this device).
|
|
48
|
+
*/
|
|
49
|
+
readonly getSiteDeviceParam: "/power_service/v1/site/get_site_device_param";
|
|
50
|
+
/**
|
|
51
|
+
* POST (encrypted+signed): write a site "device param" block. Body is
|
|
52
|
+
* `{ site_id, cmd: 246, param_type, param_data: <JSON string> }`. Used for the SOC-limit write
|
|
53
|
+
* (`param_type "27"`, `param_data` = the SocSettingParam map) — see {@link SolixClient.setSafetySocParams}.
|
|
54
|
+
*/
|
|
55
|
+
readonly setSiteDeviceParam: "/power_service/v1/site/set_site_device_param";
|
|
29
56
|
};
|
|
@@ -71,6 +71,51 @@ export declare const SOLIX_METER_FIELD_NAMES: Readonly<Record<number, string>>;
|
|
|
71
71
|
* transport). Add a meter prefix to both.
|
|
72
72
|
*/
|
|
73
73
|
export declare const SOLIX_METER_PRODUCT_PREFIXES: readonly string[];
|
|
74
|
+
/**
|
|
75
|
+
* Product-code prefix of the gen-4 Solarbank (the `ats_ax170` family, e.g. `AE103` Solarbank 4 E5000
|
|
76
|
+
* Pro) whose ff09 tag layout {@link SOLIX_SOLARBANK_FIELD_NAMES} + the SOC/temperature extraction
|
|
77
|
+
* describe. Like the meter table this is family-specific — the SAME tag carries a different quantity on
|
|
78
|
+
* the meter (`0xac` is line voltage there, battery power here), so the Solarbank names are applied ONLY
|
|
79
|
+
* to a frame from this family. A product-code prefix used to pick a decode table, not a model import.
|
|
80
|
+
* `AE10` covers the AE10x gen-4 Solarbanks and does NOT match the meter (`AE1X0`, whose 4th char is `X`).
|
|
81
|
+
*/
|
|
82
|
+
export declare const SOLIX_SOLARBANK_PRODUCT_PREFIX = "AE10";
|
|
83
|
+
/**
|
|
84
|
+
* Confirmed ff09 tag → field bindings for the gen-4 Solarbank (`ats_ax170`), correlated live against the
|
|
85
|
+
* app UI. Power values in watts; signed fields note their sign convention:
|
|
86
|
+
* - `0xac` battery power, SIGNED (+ charging / − discharging) — the measured net pack power.
|
|
87
|
+
* - `0xbc` charge power (0 unless charging); `0xad` discharge power (0 unless discharging).
|
|
88
|
+
* - `0xae` AC plug power, SIGNED (+ feeding the home / − drawing in to charge).
|
|
89
|
+
* - `0xaf` socket power — the unit's own on-board AC outlet (an appliance plugged into the Solarbank).
|
|
90
|
+
* - `0xc4` grid input power; `0xc5` home load power.
|
|
91
|
+
* SOC and temperature are NOT float channels — see {@link solixReadings}, which reads SOC from tag `0xa3`
|
|
92
|
+
* (a uint8) and temperature from the `0xa4` BMS status blob. The 4 PV-string channels (`0xc6`–`0xc9`),
|
|
93
|
+
* the AC currents (`0xb2`/`0xb3`) and export energy (`0xb4`) are not yet confirmed, so they stay raw
|
|
94
|
+
* `channel_<hex>` until a capture pins them.
|
|
95
|
+
*/
|
|
96
|
+
export declare const SOLIX_SOLARBANK_FIELD_NAMES: Readonly<Record<number, string>>;
|
|
97
|
+
/**
|
|
98
|
+
* Confirmed `state_info` tag → field bindings for the gen-4 Solarbank. `state_info` is a SEPARATE push
|
|
99
|
+
* topic from `param_info` and, though it shares the ff09 framing, its tags carry SETTINGS/targets, NOT
|
|
100
|
+
* live measurements — so the SAME tag byte means something different here than in
|
|
101
|
+
* {@link SOLIX_SOLARBANK_FIELD_NAMES} (e.g. `0xab` is live PV power in param_info, the mode's AC-socket
|
|
102
|
+
* export limit here). Mapped by live observation against the app's SOC-setting screen; everything else
|
|
103
|
+
* stays raw `state_<hex>` until confirmed the same way.
|
|
104
|
+
*/
|
|
105
|
+
export declare const SOLIX_STATE_FIELD_NAMES: Readonly<Record<number, string>>;
|
|
106
|
+
/**
|
|
107
|
+
* Decode a `state_info` ff09 frame to named + raw settings values. Skips the header tags (`< 0xa5`:
|
|
108
|
+
* request marker, serial, timestamps). Each settings tag is emitted under `state_<hex>` (a plain number
|
|
109
|
+
* so it's watchable in a consumer while more tags get mapped) AND, when confirmed, under its name from
|
|
110
|
+
* {@link SOLIX_STATE_FIELD_NAMES}. Value is read type-aware: `0x05` float32, `0x02` u16, `0x01` u8, and
|
|
111
|
+
* `0x03` the whole-number settings byte (`payload[1]`).
|
|
112
|
+
*
|
|
113
|
+
* The header cutoff is `0xa5` here, deliberately one lower than {@link solixReadings}' `0xa6` for
|
|
114
|
+
* `param_info`: the two frames are different layouts under the same ff09 framing — `state_info` carries
|
|
115
|
+
* a settings value at `0xa5` (SOC), where `param_info` has a header tag. The cutoffs are not meant to
|
|
116
|
+
* match; the spec pins `0xa5`'s treatment in each so they can't silently drift together.
|
|
117
|
+
*/
|
|
118
|
+
export declare function solixStateReadings(frame: SolixParamFrame): Record<string, number>;
|
|
74
119
|
/** Interpret one TLV value as a telemetry channel (leading type byte + payload). */
|
|
75
120
|
export declare function readSolixChannel(value: Buffer | undefined): SolixChannel | undefined;
|
|
76
121
|
/**
|
|
@@ -86,10 +131,11 @@ export declare function decodeSolixParamFrame(buf: Buffer): SolixParamFrame | nu
|
|
|
86
131
|
* carry the field count, the serial and the status, not measurements. A measurement channel is one whose
|
|
87
132
|
* leading type byte is `0x05` (float32 LE over a 4-byte payload); any other type is a non-measurement
|
|
88
133
|
* param and contributes nothing. Each measurement is emitted under `channel_<hex tag>`, and additionally
|
|
89
|
-
* under its name when the tag has a confirmed one
|
|
90
|
-
*
|
|
91
|
-
*
|
|
134
|
+
* under its name when the tag has a confirmed one AND `productCode` is from a known family — pass the
|
|
135
|
+
* telemetry topic's product code so a device outside the meter/Solarbank families keeps raw
|
|
136
|
+
* `channel_<hex>` rather than borrowing another family's tag→name table. `productCode` is required (it
|
|
92
137
|
* comes straight from the telemetry topic); pass `""` for a frame of unknown origin and no names apply.
|
|
138
|
+
* Meter family: {@link SOLIX_METER_PRODUCT_PREFIXES}; Solarbank: {@link SOLIX_SOLARBANK_PRODUCT_PREFIX}.
|
|
93
139
|
*/
|
|
94
140
|
export declare function solixReadings(frame: SolixParamFrame, productCode: string): Record<string, number>;
|
|
95
141
|
/** A live telemetry sample emitted by {@link SolixMqtt} as a `reading` event. */
|
|
@@ -176,9 +222,14 @@ export declare class SolixMqtt extends EventEmitter {
|
|
|
176
222
|
* Connect, subscribe to the device's telemetry (+ command-reply) topics, ARM realtime reporting, and
|
|
177
223
|
* start the re-arm/heartbeat timer so telemetry keeps flowing without the app. Idempotent per device.
|
|
178
224
|
*
|
|
179
|
-
* Subscribes
|
|
180
|
-
*
|
|
181
|
-
*
|
|
225
|
+
* Subscribes to `param_info` (+ the device/account command-reply channels) AND the device's `…/req`
|
|
226
|
+
* channel. `…/req` is the app→device request side — the broker copies the APP's own publishes there to
|
|
227
|
+
* any co-subscriber, so watching it lets us read a control the app changed that the telemetry does NOT
|
|
228
|
+
* reflect: the Solarbank's ambient light and display timeout ride an `…/req` cmd-17 (`0x68`) command
|
|
229
|
+
* (tags `a4`/`a5`), and the `param_info` `ba` bit only tracks OUR `set_device_attrs` write, never the
|
|
230
|
+
* app's separate command path. `onMessage` filters these — our own arming/echoes carry no
|
|
231
|
+
* `a4`/`a5` — and turns an app command into a `reading` with the app-set state. A `…/req` grant denial
|
|
232
|
+
* is non-fatal (only `param_info` is required); we just won't see app-side changes.
|
|
182
233
|
*
|
|
183
234
|
* Throws when `param_info` was not granted. A scope-denied filter comes back as SUBACK_FAILURE rather
|
|
184
235
|
* than an error (see `SecureMqtt.subscribe`), so an unusable subscription otherwise looks like
|
|
@@ -189,6 +240,14 @@ export declare class SolixMqtt extends EventEmitter {
|
|
|
189
240
|
watch(device: SolixMqttDevice): Promise<void>;
|
|
190
241
|
/** Tear down the connection and stop the re-arm timer. */
|
|
191
242
|
close(): Promise<void>;
|
|
243
|
+
/**
|
|
244
|
+
* Set a Solarbank's display screen-off timeout — publishes the captured cmd-17 command (ff09 msgtype
|
|
245
|
+
* `0x68`, tag `a5 = [01, index]`) on the device's `…/req` channel via the same envelope the arming
|
|
246
|
+
* poll uses (`sign_code:1`, no per-message signature — which the device accepts for cmd 17). `index`
|
|
247
|
+
* is the 1-based dropdown position (10s=1, 20s=2, 30s=3, 1m=4, 5m=5, 30m=6); "Never" is a separate
|
|
248
|
+
* command not handled here. Fire-and-forget: the device does not ack on a subscribed channel.
|
|
249
|
+
*/
|
|
250
|
+
setDisplayTimeout(device: SolixMqttDevice, index: number): Promise<void>;
|
|
192
251
|
/**
|
|
193
252
|
* Re-arm every watched device and send the site heartbeat. The device only pushes `param_info` while
|
|
194
253
|
* a client keeps requesting it — this replays the app's `requestDeviceInfo` (cmd 17) + `power_site`
|
|
@@ -216,8 +275,25 @@ export declare class SolixMqtt extends EventEmitter {
|
|
|
216
275
|
* Decode one inbound MQTT message envelope and emit a `reading` if it carries an ff09 param frame. The
|
|
217
276
|
* product code and the fallback serial come from the topic (`dt/{app}/{pn}/{sn}/param_info`); the frame's
|
|
218
277
|
* own `a2` field wins for the serial when it carries one.
|
|
278
|
+
*
|
|
279
|
+
* Serial resolution matters because NOT every frame carries it: the device-info frame (which alone
|
|
280
|
+
* carries SOC/temperature via tags a3/a4) has a 1-byte `a2` (a status, not a serial) and can arrive on
|
|
281
|
+
* a topic whose serial segment isn't the device serial either — leaving a `deviceSn` that matches no
|
|
282
|
+
* watched device, so a consumer keying on it would drop the reading (and its temperature). So when the
|
|
283
|
+
* resolved serial isn't a watched device, fall back to the single watched device of this product code.
|
|
219
284
|
*/
|
|
220
285
|
private onMessage;
|
|
286
|
+
/**
|
|
287
|
+
* Turn an app→device cmd-17 (`0x68`) command seen on the `…/req` channel into a `reading` carrying the
|
|
288
|
+
* app-set control state, so a change made in the app reflects back. The Solarbank's ambient light and
|
|
289
|
+
* display timeout are set this way (byte-identical to what {@link setDisplayTimeout} publishes), and the
|
|
290
|
+
* broker copies the app's publish to us as a co-subscriber. Only `0x68` frames carrying `a4`/`a5` are
|
|
291
|
+
* emitted, so the arming polls (`0x40`/`0x57`) and our own echoes contribute nothing:
|
|
292
|
+
* - `a4 = [01, s]` → ambient light, INVERTED (`s` 0 = on) → `ambientLightOn` 1/0. The `ba` telemetry
|
|
293
|
+
* bit only tracks our `set_device_attrs` write, so this is the ONLY read-back of an app light toggle.
|
|
294
|
+
* - `a5 = [01, i]` → display timeout, `i` = 1-based dropdown index → `displayTimeoutIndex`.
|
|
295
|
+
*/
|
|
296
|
+
private handleCommand;
|
|
221
297
|
}
|
|
222
298
|
/**
|
|
223
299
|
* Pull the ff09 binary frame out of a received message. Solix telemetry arrives as a `{head, payload}`
|
|
@@ -235,4 +311,11 @@ export declare function extractFf09Payload(raw: unknown): Buffer | null;
|
|
|
235
311
|
* `fe` carries a fresh unix-timestamp nonce; the trailing byte is XOR of every preceding byte (the same
|
|
236
312
|
* checksum the meter's telemetry frames use — verified to reproduce the captured frames exactly).
|
|
237
313
|
*/
|
|
314
|
+
/**
|
|
315
|
+
* Build the display screen-off-timeout command frame (ff09 msgtype `0x68`, tag `a5 = [01, index]`),
|
|
316
|
+
* captured live from the app on `cmd/anker_power/<pc>/<sn>/req` (cmd 17). `index` is the 1-based
|
|
317
|
+
* position in the app dropdown `[10s,20s,30s,1m,5m,30m]` — live-confirmed 10s=1, 30s=3, 1m=4. "Never"
|
|
318
|
+
* is a separate command (not this one). Byte-identical to the captured frames modulo the index byte.
|
|
319
|
+
*/
|
|
320
|
+
export declare function buildDisplayTimeoutFrame(index: number): Buffer;
|
|
238
321
|
export declare function buildFf09Request(variant: "info" | "realtime", atUnixSec?: number): Buffer;
|
|
@@ -80,14 +80,24 @@ export interface ParsedTopic {
|
|
|
80
80
|
export declare function parseSecureTopic(topic: string): ParsedTopic | undefined;
|
|
81
81
|
/** The per-device Solix topics for `{appName, productCode, deviceSn}`. */
|
|
82
82
|
export interface SolixDeviceTopics {
|
|
83
|
-
/** Telemetry the device pushes (SUBSCRIBE) — ff09 `param_info` frames. */
|
|
83
|
+
/** Telemetry the device pushes (SUBSCRIBE) — ff09 `param_info` frames (live measurements). */
|
|
84
84
|
paramInfo: string;
|
|
85
|
+
/**
|
|
86
|
+
* Settings/state the device pushes (SUBSCRIBE) — ff09 `state_info` frames. Same ff09 framing as
|
|
87
|
+
* `param_info` but the TAGS carry SETTINGS/targets (mode export limit, SOC limits, max_load, toggles),
|
|
88
|
+
* NOT live measurements — so it needs its own tag→name table, not the param_info one.
|
|
89
|
+
*/
|
|
90
|
+
stateInfo: string;
|
|
85
91
|
/** This device's command replies (SUBSCRIBE). */
|
|
86
92
|
cmdRes: string;
|
|
87
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* The device's requestDeviceInfo channel (cmd 17). PUBLISH to arm reporting; also SUBSCRIBE — the
|
|
95
|
+
* broker copies the APP's publishes here to any co-subscriber, which is the only way to observe a
|
|
96
|
+
* control the app changed that `param_info` does not reflect (ambient light, display timeout).
|
|
97
|
+
*/
|
|
88
98
|
req: string;
|
|
89
99
|
}
|
|
90
|
-
/** Build the per-device Solix topics. `param_info` is the telemetry we decode; `req` is
|
|
100
|
+
/** Build the per-device Solix topics. `param_info` is the telemetry we decode; `req` is arm + read-back. */
|
|
91
101
|
export declare function solixDeviceTopics(appName: string, productCode: string, deviceSn: string): SolixDeviceTopics;
|
|
92
102
|
/** The per-account Solix topics keyed by `user_id`. */
|
|
93
103
|
export interface SolixUserTopics {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mega-yfue/eufy-sdk",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.18",
|
|
4
4
|
"description": "One typed TypeScript client for the Anker eufy v6 cloud — capability-driven devices, realtime events over P2P/MQTT/push, and live media",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "mega-yfue",
|