@mega-yfue/eufy-sdk 0.2.0-beta.16 → 0.2.0-beta.17

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.
@@ -14,26 +14,27 @@ import type { Surface } from "./members.js";
14
14
  /** Every capability a Solix device may carry. Solix's OWN union (not eufy's `Capability`). */
15
15
  export type SolixCapability = "identity" | "firmware" | "connectivity" | "energyMeter" | "battery" | "solarInput" | "acOutput" | "evCharger" | "charger" | "cooler";
16
16
  /**
17
- * The `energyMeter` surface, declared once. Only the ONE confirmed tag→name binding is a member:
18
- * `meterVoltageL1` (ff09 tag `0xAC`), confirmed against a live single-phase frame. The evidence gate
19
- * (`bindMembers`) installs its getter only once a frame carrying tag `0xAC` has landed and answers
20
- * `undefined` before.
17
+ * The `energyMeter` surface — the electrical readings the vendor app names, as typed members. Each is
18
+ * read-only and evidence-gated: `bindMembers` installs a getter only once a frame that REPORTS that tag
19
+ * has landed, and answers `undefined` (not a fabricated `0`) before any has. The gate is "reported", not
20
+ * "non-zero": a single-phase / single-CT meter still reports its L2/L3 slots as `0.0`, so those members
21
+ * install and read `0` rather than staying absent — a caller sees `0` for an idle phase, not `undefined`.
21
22
  *
22
- * The meter reports many more quantities (per-line power/current/voltage, totals, import/export energy),
23
- * but their tag→name bindings are a structural inference not yet pinned to a known-load capture. Rather
24
- * than assert a name that could mislabel a live float, those stay reachable raw as `channel_<hex>` from
25
- * the device's telemetry (a static members table cannot enumerate dynamic hex tags); each is promoted to
26
- * a member here, one line, as a capture confirms its binding.
23
+ * Provenance splits by what the evidence actually pins. The app's field VOCABULARY (these twelve names)
24
+ * is authoritative. For the tag→field binding, a live single-phase frame confirmed the L1 and total
25
+ * magnitudes (a nominal mains voltage, an equal line/total power pair, the line current), so those four
26
+ * positions are `verified`. The L2/L3 tags are never non-zero on a single-CT install and the app itself
27
+ * receives the meter as named JSON — there is no tag→phase decoder in the app binary — so their phase
28
+ * assignment is inferred from the block ordering and is marked `guessed`, not `apk`. The energy counters (`meterImportEnergy`/`meterExportEnergy`) are named
29
+ * on the wire but are NOT members here: their tag→name is confirmed, but their unit SCALE is not (a live
30
+ * reading is consistent with either Wh or kWh), so they stay raw named values via
31
+ * {@link SOLIX_METER_FIELD_NAMES} until a capture pins the scale, rather than ship a member with a guessed unit.
27
32
  *
28
33
  * @internal — the declaration `SolixEnergyMeterReads` derives from; exported (like the eufy `*_MEMBERS`
29
34
  * tables) so it is a known symbol, but excluded from the rendered API reference.
30
35
  */
31
36
  export declare const SOLIX_ENERGY_METER_MEMBERS: {
32
- /**
33
- * Line-1 voltage (V), ff09 tag `0xAC` — the ONE confirmed meter binding, matched against a live
34
- * single-phase frame (a nominal mains voltage). Read-only; the evidence gate installs its getter only
35
- * once a frame carrying `0xAC` has landed, so it is absent (not a fabricated `0`) until then.
36
- */
37
+ /** Line-1 voltage (V), ff09 tag `0xAC` — confirmed live (a nominal mains voltage). */
37
38
  readonly meterVoltageL1: {
38
39
  readonly param: 172;
39
40
  readonly type: "number";
@@ -42,6 +43,87 @@ export declare const SOLIX_ENERGY_METER_MEMBERS: {
42
43
  readonly provenance: "verified";
43
44
  readonly description: "Meter line-1 voltage (V) — ff09 tag 0xAC, confirmed against a live single-phase frame.";
44
45
  };
46
+ /** Line-2 voltage (V), ff09 tag `0xAD` — the app's field; reads 0 until a multi-phase frame carries it. */
47
+ readonly meterVoltageL2: {
48
+ readonly param: 173;
49
+ readonly type: "number";
50
+ readonly kind: "scalar";
51
+ readonly unit: "V";
52
+ readonly provenance: "guessed";
53
+ readonly description: "Meter line-2 voltage (V) — ff09 tag 0xAD; phase assignment inferred from block ordering (never observed non-zero), 0 on a single-phase install.";
54
+ };
55
+ /** Line-3 voltage (V), ff09 tag `0xAE` — the app's field; reads 0 until a multi-phase frame carries it. */
56
+ readonly meterVoltageL3: {
57
+ readonly param: 174;
58
+ readonly type: "number";
59
+ readonly kind: "scalar";
60
+ readonly unit: "V";
61
+ readonly provenance: "guessed";
62
+ readonly description: "Meter line-3 voltage (V) — ff09 tag 0xAE; phase assignment inferred from block ordering (never observed non-zero), 0 on a single-phase install.";
63
+ };
64
+ /** Line-1 current (A), ff09 tag `0xAF` — confirmed live (the line's CT current). */
65
+ readonly meterCurrentL1: {
66
+ readonly param: 175;
67
+ readonly type: "number";
68
+ readonly kind: "scalar";
69
+ readonly unit: "A";
70
+ readonly provenance: "verified";
71
+ readonly description: "Meter line-1 current (A) — ff09 tag 0xAF, confirmed against a live single-phase frame.";
72
+ };
73
+ /** Line-2 current (A), ff09 tag `0xB0` — the app's field; reads 0 until a multi-phase frame carries it. */
74
+ readonly meterCurrentL2: {
75
+ readonly param: 176;
76
+ readonly type: "number";
77
+ readonly kind: "scalar";
78
+ readonly unit: "A";
79
+ readonly provenance: "guessed";
80
+ readonly description: "Meter line-2 current (A) — ff09 tag 0xB0; phase assignment inferred from block ordering (never observed non-zero), 0 on a single-phase install.";
81
+ };
82
+ /** Line-3 current (A), ff09 tag `0xB1` — the app's field; reads 0 until a multi-phase frame carries it. */
83
+ readonly meterCurrentL3: {
84
+ readonly param: 177;
85
+ readonly type: "number";
86
+ readonly kind: "scalar";
87
+ readonly unit: "A";
88
+ readonly provenance: "guessed";
89
+ readonly description: "Meter line-3 current (A) — ff09 tag 0xB1; phase assignment inferred from block ordering (never observed non-zero), 0 on a single-phase install.";
90
+ };
91
+ /** Line-1 active power (W), ff09 tag `0xA8` — confirmed live; negative on export. */
92
+ readonly meterPowerL1: {
93
+ readonly param: 168;
94
+ readonly type: "number";
95
+ readonly kind: "scalar";
96
+ readonly unit: "W";
97
+ readonly provenance: "verified";
98
+ readonly description: "Meter line-1 active power (W) — ff09 tag 0xA8, confirmed live; negative on export.";
99
+ };
100
+ /** Line-2 active power (W), ff09 tag `0xA9` — the app's field; reads 0 until a multi-phase frame carries it. */
101
+ readonly meterPowerL2: {
102
+ readonly param: 169;
103
+ readonly type: "number";
104
+ readonly kind: "scalar";
105
+ readonly unit: "W";
106
+ readonly provenance: "guessed";
107
+ readonly description: "Meter line-2 active power (W) — ff09 tag 0xA9; phase assignment inferred from block ordering (never observed non-zero), 0 on a single-phase install.";
108
+ };
109
+ /** Line-3 active power (W), ff09 tag `0xAA` — the app's field; reads 0 until a multi-phase frame carries it. */
110
+ readonly meterPowerL3: {
111
+ readonly param: 170;
112
+ readonly type: "number";
113
+ readonly kind: "scalar";
114
+ readonly unit: "W";
115
+ readonly provenance: "guessed";
116
+ readonly description: "Meter line-3 active power (W) — ff09 tag 0xAA; phase assignment inferred from block ordering (never observed non-zero), 0 on a single-phase install.";
117
+ };
118
+ /** Aggregate active power (W), ff09 tag `0xAB` — confirmed live; equals line-1 on a single phase. */
119
+ readonly meterPowerTotal: {
120
+ readonly param: 171;
121
+ readonly type: "number";
122
+ readonly kind: "scalar";
123
+ readonly unit: "W";
124
+ readonly provenance: "verified";
125
+ readonly description: "Meter total active power (W) — ff09 tag 0xAB, confirmed live; equals L1 on one phase.";
126
+ };
45
127
  };
46
128
  /** Bound `energyMeter` reads (the members-derived half of `dev.energyMeter()`). Read-only. */
47
129
  export type SolixEnergyMeterReads = Surface<typeof SOLIX_ENERGY_METER_MEMBERS>;
@@ -52,7 +134,12 @@ export type SolixEnergyMeterReads = Surface<typeof SOLIX_ENERGY_METER_MEMBERS>;
52
134
  * Unlisted categories contribute nothing here.
53
135
  */
54
136
  export declare const CATEGORY_CAPABILITIES: Readonly<Record<string, readonly SolixCapability[]>>;
55
- /** Product-code prefixes known to be grid/energy meters (detects `energyMeter` regardless of category). */
137
+ /**
138
+ * Product-code prefixes known to be grid/energy meters (detects `energyMeter` regardless of category).
139
+ * Keep in lockstep with `SOLIX_METER_PRODUCT_PREFIXES` in `transport/mqtt/solix-mqtt.ts` (the same meter
140
+ * prefixes, transport-side, that gate the tag→name table): a prefix added here but not there grants
141
+ * `energyMeter` to a device whose frames the decoder then refuses to name. Add a meter prefix to both.
142
+ */
56
143
  export declare const SOLIX_METER_MODELS: readonly string[];
57
144
  /**
58
145
  * Product-code prefixes for the grid-tie Solarbank / home-battery family (detects `battery` +
@@ -34,22 +34,43 @@ export interface SolixParamFrame {
34
34
  }
35
35
  /**
36
36
  * Telemetry field tags for the Smart Meter (AE1X0) that we emit under a stable NAME, keyed by ff09 tag
37
- * byte. Only tags whose tag→name binding is CONFIRMED against a live frame live here:
37
+ * byte. These twelve are the meter fields the vendor app itself names, and their tag→name bindings are
38
+ * confirmed:
38
39
  *
39
- * - `0xac` = `meterVoltageL1` — confirmed against live single-phase data (a nominal mains voltage).
40
+ * - The app's field vocabulary is exactly these twelve — voltage, current and power per line
41
+ * (L1/L2/L3), a power total, and cumulative import/export energy — with no current total, no
42
+ * frequency and no power-factor field.
43
+ * - A live single-phase frame confirms the tag→field magnitudes: `0xac` a nominal mains voltage,
44
+ * `0xa8` == `0xab` an equal power pair (line power equals total on one phase, one of them going
45
+ * negative on export), `0xaf` the line current, `0xb3` a slowly-cumulative import counter; the L2/L3
46
+ * slots read 0 on a single-CT install.
40
47
  *
41
- * Every other measurement tag still surfaces as `channel_<hex tag>` (see {@link solixReadings}), so
42
- * nothing on the wire is lost — a caller reads unconfirmed tags there. The names are deliberately NOT
43
- * asserted for the rest: the app exposes the field *list*, but the tag→name *binding* below is a
44
- * structural inference until a known-load capture pins it, and a mislabelled live float is worse than an
45
- * honest `channel_<tag>`. The recovered candidates, to re-add one line each (moving the tag from this
46
- * comment to the map above) as a known-load capture confirms each binding:
48
+ * The frame carries sixteen float slots (`0xa8`..`0xb7`). The four that name no field — `0xb2`, `0xb5`,
49
+ * `0xb6`, `0xb7` — stay raw `channel_<hex tag>` (see {@link solixReadings}). `0xb2` in particular is NOT
50
+ * a current total: under a 1.371 A line current it reads 0.009, three orders of magnitude off. Both
51
+ * `0xb2` and `0xb7` read zero at idle and non-zero under load, so they carry *something* load-related;
52
+ * what, is not established.
47
53
  *
48
- * 0xa8 meterPowerL1 0xa9 meterPowerL2 0xaa meterPowerL3 0xab meterPowerTotal
49
- * 0xad meterVoltageL2 0xae meterVoltageL3 0xaf meterCurrentL1 0xb0 meterCurrentL2
50
- * 0xb1 meterCurrentL3 0xb2 meterCurrentTotal 0xb3 meterImportEnergy 0xb4 meterExportEnergy
54
+ * This table is **meter-family-specific**: the same tag carries a different quantity on another Solix
55
+ * device (a Solarbank's `0xac` reads a power value, not a voltage), so {@link solixReadings} applies
56
+ * these names ONLY to a frame from the meter family — see {@link SOLIX_METER_PRODUCT_PREFIXES}. Every
57
+ * measurement tag still surfaces as `channel_<hex tag>` regardless of device, so nothing on the wire is
58
+ * lost; the model layer names non-meter tags per capability.
51
59
  */
52
60
  export declare const SOLIX_METER_FIELD_NAMES: Readonly<Record<number, string>>;
61
+ /**
62
+ * Product-code prefixes of the Smart Meter family that {@link SOLIX_METER_FIELD_NAMES} decodes. The table
63
+ * is meter-specific, so {@link solixReadings} applies its named fields ONLY to a frame whose product code
64
+ * starts with one of these; a Solarbank (`AE103`) reporting the same `0xac` tag would otherwise be
65
+ * mislabelled `meterVoltageL1` with a nonsensical (negative-power) value. These are product-code prefixes
66
+ * used to select a decode table — not a model import — so the `transport ⊥ model` rule is untouched.
67
+ *
68
+ * Keep this in lockstep with `SOLIX_METER_MODELS` in `model/capabilities/solix.ts` (the same meter
69
+ * prefixes, model-side): a prefix added there but not here grants `energyMeter` to a device whose frames
70
+ * this decoder then refuses to name, and no guard can catch the split (the model layer can't import
71
+ * transport). Add a meter prefix to both.
72
+ */
73
+ export declare const SOLIX_METER_PRODUCT_PREFIXES: readonly string[];
53
74
  /** Interpret one TLV value as a telemetry channel (leading type byte + payload). */
54
75
  export declare function readSolixChannel(value: Buffer | undefined): SolixChannel | undefined;
55
76
  /**
@@ -65,9 +86,12 @@ export declare function decodeSolixParamFrame(buf: Buffer): SolixParamFrame | nu
65
86
  * carry the field count, the serial and the status, not measurements. A measurement channel is one whose
66
87
  * leading type byte is `0x05` (float32 LE over a 4-byte payload); any other type is a non-measurement
67
88
  * param and contributes nothing. Each measurement is emitted under `channel_<hex tag>`, and additionally
68
- * under its name when the tag has a confirmed one in {@link SOLIX_METER_FIELD_NAMES}.
89
+ * under its name when the tag has a confirmed one in {@link SOLIX_METER_FIELD_NAMES} AND `productCode` is
90
+ * from the meter family (see {@link SOLIX_METER_PRODUCT_PREFIXES}) — so a non-meter device's tags stay
91
+ * raw `channel_<hex>` rather than borrowing the meter's tag→name table. `productCode` is required (it
92
+ * comes straight from the telemetry topic); pass `""` for a frame of unknown origin and no names apply.
69
93
  */
70
- export declare function solixReadings(frame: SolixParamFrame): Record<string, number>;
94
+ export declare function solixReadings(frame: SolixParamFrame, productCode: string): Record<string, number>;
71
95
  /** A live telemetry sample emitted by {@link SolixMqtt} as a `reading` event. */
72
96
  export interface SolixReading {
73
97
  deviceSn: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mega-yfue/eufy-sdk",
3
- "version": "0.2.0-beta.16",
3
+ "version": "0.2.0-beta.17",
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",