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

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.
@@ -481,7 +481,7 @@ export declare function buildActions(caps: readonly Capability[], deps: MemberDe
481
481
  * @internal
482
482
  */
483
483
  export declare function describeCapabilities(bound: Partial<DeviceActionMap>, ctx?: AvailabilityContext): CapabilityDescriptor[];
484
- export type { CommandContext, CapabilityActions, CapabilityStateReader, CapabilityModule, DetectionSpec, CapabilityFrame, CapabilityEvent, InboundSignal, EventMapping, ProductLine, ActionArgSpec, DecodedState, } from "./types.js";
484
+ export type { CommandContext, CapabilityActions, CapabilityStateReader, CapabilityModule, DetectionSpec, CapabilityFrame, CapabilityEvent, InboundSignal, EventClaim, EventMapping, ProductLine, ActionArgSpec, DecodedState, } from "./types.js";
485
485
  /**
486
486
  * The value-kind vocabulary a read is annotated with, re-exported from the barrel that publishes the
487
487
  * read itself, so the union a member's `kind` is drawn from is reachable from the same import.
@@ -136,6 +136,45 @@ export interface EventMapping {
136
136
  * capture. Return `{}` when this signal doesn't carry the field, so nothing is invented.
137
137
  */
138
138
  derive?(signal: InboundSignal): Record<string, unknown>;
139
+ /**
140
+ * Evidence that this device deals in the classification the id names, for an id whose presence in
141
+ * the wire vocabulary does not prove it.
142
+ *
143
+ * The AI-detection ids are shared verbatim across the camera families, so the id space says what an
144
+ * integer MEANS and never which units classify that way. A claim is how a mapping states the
145
+ * evidence that separates them, and it narrows the DESCRIPTION only: {@link EventMapping} stays in
146
+ * the dispatch index unclaimed, so a device that sends the push still gets the event. Under-reporting
147
+ * what a device is expected to emit is recoverable; dropping an event it did emit is not.
148
+ *
149
+ * Every field must hold — the AND to {@link DetectionSpec}'s OR, because a claim rules a family OUT
150
+ * rather than finding one more reason to say yes. A fact the context does not carry rules nothing
151
+ * out: only evidence that positively contradicts the claim withdraws the event.
152
+ */
153
+ claim?: EventClaim;
154
+ }
155
+ /**
156
+ * Evidence separating the families that share one inbound id.
157
+ *
158
+ * Both fields are optional and independent; an empty claim asserts nothing and is the same as none.
159
+ */
160
+ export interface EventClaim {
161
+ /**
162
+ * The codecs whose devices issue the id. For an id drawn from a vocabulary one device family owns:
163
+ * the AI-detection ids belong to the camera families, and a standalone sensor announces its own
164
+ * motion under a different id entirely.
165
+ */
166
+ codecs?: readonly Codec[];
167
+ /**
168
+ * Member names whose INSTALLED getter is the evidence — the device reported the parameter behind
169
+ * the classification, which is the same bar every typed read is held to.
170
+ */
171
+ reads?: readonly string[];
172
+ /**
173
+ * The topology the id belongs to: `true` for an id only a station's attached device sends, `false`
174
+ * for one only a standalone unit sends. Compared against {@link AvailabilityContext.homeBaseAttached},
175
+ * and ignored where that is absent.
176
+ */
177
+ homeBaseAttached?: boolean;
139
178
  }
140
179
  /**
141
180
  * A decoded inbound event a capability wants surfaced on the SDK. `event` is the EufyMega event
@@ -197,6 +236,15 @@ export interface AvailabilityContext {
197
236
  * `undefined` as an empty set — `ctx.paramIds?.has(dp) ?? false`.
198
237
  */
199
238
  paramIds?: ReadonlySet<number>;
239
+ /**
240
+ * Whether the device hangs off a HomeBase (a `parent_sn` other than its own) rather than standing
241
+ * alone. A DEVICE fact, not a transport one — the same class of routing evidence as {@link hasP2p} —
242
+ * which is why it sits here rather than on {@link CommandContext}: it is as true of a described
243
+ * device as of a commanded one. The `rtsp` capability gates on it because a station serves an
244
+ * attached camera's stream itself and ignores that camera's authentication setting, so the write
245
+ * cannot do what its name promises there.
246
+ */
247
+ homeBaseAttached?: boolean;
200
248
  }
201
249
  export interface CommandContext extends AvailabilityContext {
202
250
  /** Device channel (0 for standalone, `device_channel` on a HomeBase). */
@@ -259,13 +307,6 @@ export interface CommandContext extends AvailabilityContext {
259
307
  * capability uses this to route lock/unlock to P2P vs. reject with a clear MQTT-not-wired error.
260
308
  */
261
309
  hasP2p?: boolean;
262
- /**
263
- * Whether the device hangs off a HomeBase (a `parent_sn` other than its own) rather than standing
264
- * alone. A DEVICE fact, not a transport one — the same class of routing evidence as {@link hasP2p}.
265
- * The `rtsp` capability gates on it because a station serves an attached camera's stream itself and
266
- * ignores that camera's authentication setting, so the write cannot do what its name promises there.
267
- */
268
- homeBaseAttached?: boolean;
269
310
  /**
270
311
  * Parsed `get_product_data_point` catalog for this device's SKU — present for vacuum/mower devices,
271
312
  * absent for all other codecs. Capabilities use it for per-model feature-availability and value-range
@@ -379,6 +379,23 @@ export type CarpetStrategy = (typeof CARPET_STRATEGIES)[number];
379
379
  */
380
380
  export declare const CLEAN_EXTENTS: readonly ["normal", "narrow", "quick"];
381
381
  export type CleanExtent = (typeof CLEAN_EXTENTS)[number];
382
+ /**
383
+ * Build a `CleanParamRequest` (DP 154) stating the three cleaning settings a run uses.
384
+ *
385
+ * The message is `{ clean_param: CleanParam }` — field 1 of the request, carrying the same `CleanParam`
386
+ * this module decodes out of field 1 of the reports it receives, so the shape a write sends is the
387
+ * shape a read has already been proven against on live hardware.
388
+ *
389
+ * All three settings are stated together because one message carries all three: a write naming fewer
390
+ * would be a `CleanParam` with the rest silent, and what a robot does with a half-stated one is not
391
+ * something this SDK has observed. `clean_times`(7) is never written — the vendor's own field comment
392
+ * makes zero mean "not stated", so omitting it leaves the robot's configured pass count alone — and
393
+ * neither is `fan`(6), which belongs to the suction capability's own data point.
394
+ *
395
+ * {@link VACUUM_CLEAN_MEMBERS.setCleanParam} dispatches this.
396
+ * @internal
397
+ */
398
+ export declare function encodeCleanParam(cleanType: VacuumCleanType, cleanExtent: CleanExtent, mopLevel: MopLevel): string;
382
399
  /**
383
400
  * Read one setting out of the CONFIGURED `CleanParam` (DP 154), by its field number.
384
401
  *
@@ -1963,6 +1980,48 @@ export declare const VACUUM_CLEAN_MEMBERS: {
1963
1980
  readonly startScene: import("./members.js").MethodMember<(sceneId: number) => Promise<void>> & {
1964
1981
  available: (ctx: import("./types.js").CommandContext) => boolean;
1965
1982
  };
1983
+ /**
1984
+ * State the cleaning settings a run uses — `CleanParamRequest.clean_param` over DP 154.
1985
+ *
1986
+ * The write counterpart of {@link VACUUM_CLEAN_MEMBERS.cleanType},
1987
+ * {@link VACUUM_CLEAN_MEMBERS.cleanExtent} and {@link VACUUM_CLEAN_MEMBERS.mopLevel}: one message
1988
+ * carries all three, so they are set together rather than through three setters that would each send
1989
+ * the same message with the other two silent.
1990
+ *
1991
+ * **The evidence, and its limit.** The frame is field 1 of `CleanParamRequest`, which carries the
1992
+ * very `CleanParam` this module decodes out of field 1 of the reports a live T2351 sends — the
1993
+ * field numbers, the single-field wrappers and the `mop_mode.level` scale are all read off that
1994
+ * capture, and `encodeCleanParam` writes what `decodeCleanParamValue` reads. What is NOT captured is
1995
+ * the write direction itself. It ships as a method rather than an unverified write because the
1996
+ * hazard that rule answers does not arise here: DP 154 is the robot's own settings report, so a frame
1997
+ * it does not accept leaves those three reads unchanged, where a wrong fire-and-forget command would
1998
+ * look exactly like success.
1999
+ *
2000
+ * Suction is not here. It has its own data point and its own capability — a `fan` field exists in
2001
+ * this message and is deliberately not written, for the same reason it is not read.
2002
+ */
2003
+ readonly setCleanParam: {
2004
+ readonly method: (deps: import("./members.js").MemberDeps) => (cleanType: VacuumCleanType, cleanExtent: CleanExtent, mopLevel: MopLevel) => Promise<void>;
2005
+ readonly description: string;
2006
+ readonly available: ((ctx: import("./types.js").CommandContext) => boolean) & ((ctx: import("./types.js").CommandContext) => boolean);
2007
+ readonly answers?: true;
2008
+ readonly args: readonly [{
2009
+ readonly name: "cleanType";
2010
+ readonly kind: "enum";
2011
+ readonly values: readonly ["sweep", "mop", "sweepAndMop", "sweepThenMop"];
2012
+ readonly description: "What the robot does with a surface.";
2013
+ }, {
2014
+ readonly name: "cleanExtent";
2015
+ readonly kind: "enum";
2016
+ readonly values: readonly ["normal", "narrow", "quick"];
2017
+ readonly description: "How far past the mapped edge a job reaches. Wire order, not app order.";
2018
+ }, {
2019
+ readonly name: "mopLevel";
2020
+ readonly kind: "enum";
2021
+ readonly values: readonly ["low", "middle", "high"];
2022
+ readonly description: "How much water the mop lays down. Only meaningful for a clean type that mops.";
2023
+ }];
2024
+ };
1966
2025
  /**
1967
2026
  * Clean the named rooms of a named map (ModeCtrlRequest method 1 over DP 152).
1968
2027
  *
@@ -5,3 +5,4 @@ export * from "./broker-discovery.js";
5
5
  export * from "./biz-stream.js";
6
6
  export { SolixMqtt } from "./solix-mqtt.js";
7
7
  export type { SolixMqttOptions, SolixMqttDevice, SolixReading, SolixParamFrame, SolixChannel } from "./solix-mqtt.js";
8
+ export { SOLIX_MODBUS_EMS_MODES } from "./solix-mqtt.js";
@@ -46,10 +46,20 @@ export interface SolixParamFrame {
46
46
  * slots read 0 on a single-CT install.
47
47
  *
48
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.
49
+ * `0xb6`, `0xb7` — stay raw `channel_<hex tag>` (see {@link solixReadings}). Both `0xb2` and `0xb7` read
50
+ * zero at idle and non-zero under load, so they carry *something* load-related; what, is not established.
51
+ * `0xb2` is dimensionally consistent with **power factor** and rules **reactive power** out: on the same
52
+ * frame the line reads ~240 V at 1.371 A (apparent power S = V·I ≈ 328 VA), so a reactive-power slot would
53
+ * read in the hundreds of VAR, not `0xb2`'s 0.009 — whereas a power factor P/S is a sub-unity ratio of the
54
+ * right magnitude (~0.008). It is left raw regardless, since a single frame doesn't pin it. `0xb7` (~0.1
55
+ * under load) has no such magnitude tell and stays fully open.
56
+ *
57
+ * Each name is annotated with the equivalent register from Anker's OWN vendor integration for the
58
+ * newer Modbus-TCP meter generation (Smart Meter Gen 2), which independently corroborates the meaning
59
+ * of each tag: our `meterPowerL1` is their `primary_phase_1_active_power`, and so on. Same physical
60
+ * quantities, different hardware/transport (their meter reports two CT channels — `primary` and
61
+ * `secondary` — and also exposes `reactive_power`, `power_factor` and per-phase energy, none of which
62
+ * this single-channel ff09 frame carries).
53
63
  *
54
64
  * This table is **meter-family-specific**: the same tag carries a different quantity on another Solix
55
65
  * device (a Solarbank's `0xac` reads a power value, not a voltage), so {@link solixReadings} applies
@@ -92,6 +102,13 @@ export declare const SOLIX_SOLARBANK_PRODUCT_PREFIX = "AE10";
92
102
  * (a uint8) and temperature from the `0xa4` BMS status blob. The 4 PV-string channels (`0xc6`–`0xc9`),
93
103
  * the AC currents (`0xb2`/`0xb3`) and export energy (`0xb4`) are not yet confirmed, so they stay raw
94
104
  * `channel_<hex>` until a capture pins them.
105
+ *
106
+ * Names are annotated with the equivalent register from Anker's OWN vendor integration for the newer
107
+ * Modbus-TCP Solarbank generation (which includes a "Solarbank 4 E5000 Pro" config — the same product as
108
+ * `AE103`, a newer hardware rev), cross-checking each meaning. Their integration splits our signed
109
+ * `batteryPower` into `battery_charging_power` / `battery_discharging_power` off one register, and exposes
110
+ * a single `pv_power` total rather than our four per-string channels; `socketPower` (the on-board AC
111
+ * outlet) has no register there. Same quantities, different transport.
95
112
  */
96
113
  export declare const SOLIX_SOLARBANK_FIELD_NAMES: Readonly<Record<number, string>>;
97
114
  /**
@@ -103,6 +120,28 @@ export declare const SOLIX_SOLARBANK_FIELD_NAMES: Readonly<Record<number, string
103
120
  * stays raw `state_<hex>` until confirmed the same way.
104
121
  */
105
122
  export declare const SOLIX_STATE_FIELD_NAMES: Readonly<Record<number, string>>;
123
+ /**
124
+ * The Solarbank EMS `operating_mode` enumeration from Anker's OWN vendor integration for the newer
125
+ * **Modbus-TCP** hardware rev (Solarbank 4 E5000 Pro, register `operating_mode` gated by the `0x8006`
126
+ * capability mask). Value → English label:
127
+ * - `0` selfConsumption — "Self-Consumption Mode"
128
+ * - `1` timeOfUse — "Time Of Use Mode"
129
+ * - `3` thirdPartyControl — "Third-Party Controlled"
130
+ * - `4` custom — "Custom Mode"
131
+ * - `5` socketOverlay — "Socket Overlay Mode"
132
+ * - `6` smart — "Smart Mode"
133
+ * - `7` dynamicTariff — "Dynamic Tariff Mode"
134
+ *
135
+ * Value `2` is unassigned there — seven modes across `{0,1,3,4,5,6,7}`, not eight.
136
+ *
137
+ * NOT a decoder for this SDK's ff09 `mode` (`state_info` tag `0xa9`): that OLDER cloud/MQTT `AE103`
138
+ * numbering is DIFFERENT on every value — `1`=custom, `2`=self-consumption, `4`=rapid charge, `7`=smart,
139
+ * `8`=dynamic tariff (recorded on the `0xa9` field above, correlated against the app). Labelling an ff09
140
+ * `mode` value with this Modbus map would be confidently wrong. It is exported as the vendor's own
141
+ * reference enumeration and the thing an AE103 `0xa9` correlation capture would be checked against —
142
+ * fold the two only if such a capture proves the numbers match.
143
+ */
144
+ export declare const SOLIX_MODBUS_EMS_MODES: Readonly<Record<number, string>>;
106
145
  /**
107
146
  * Decode a `state_info` ff09 frame to named + raw settings values. Skips the header tags (`< 0xa5`:
108
147
  * request marker, serial, timestamps). Each settings tag is emitted under `state_<hex>` (a plain number
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mega-yfue/eufy-sdk",
3
- "version": "0.2.0-beta.21",
3
+ "version": "0.2.0-beta.23",
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",