@mega-yfue/eufy-sdk 0.2.0-beta.1 → 0.2.0-beta.11
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/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 +36 -0
- package/dist/core/store.d.ts +20 -9
- package/dist/index.js +1446 -192
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/arming.d.ts +58 -28
- package/dist/model/capabilities/display.d.ts +85 -0
- package/dist/model/capabilities/index.d.ts +11 -5
- package/dist/model/capabilities/solix.d.ts +75 -0
- package/dist/model/capabilities/types.d.ts +16 -4
- package/dist/model/capabilities/vacuum-clean.d.ts +74 -25
- package/dist/model/index.d.ts +3 -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 +20 -0
- package/dist/model/solix-device.d.ts +102 -0
- package/dist/model/types.d.ts +5 -5
- package/dist/transport/ff09.d.ts +7 -0
- package/dist/transport/http/index.d.ts +1 -0
- package/dist/transport/http/solix-client.d.ts +158 -0
- package/dist/transport/http/solix-constants.d.ts +29 -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 +214 -0
- package/dist/transport/mqtt/topics.d.ts +20 -0
- package/dist/transport/p2p/command-router.d.ts +143 -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 +100 -6
- package/dist/transport/p2p/media.d.ts +11 -0
- package/dist/transport/p2p/p2p-session.d.ts +4 -0
- package/dist/transport/p2p/session-manager.d.ts +57 -23
- package/dist/transport/p2p/shared-live-source.d.ts +10 -1
- package/package.json +3 -2
|
@@ -2,25 +2,58 @@ import { type Surface } from "./members.js";
|
|
|
2
2
|
import type { CapabilityModule, CommandContext } from "./types.js";
|
|
3
3
|
import type { Command } from "../../core/contracts.js";
|
|
4
4
|
/**
|
|
5
|
-
* The guard modes
|
|
6
|
-
*
|
|
5
|
+
* The guard modes `setMode` can SET — every mode a station reports, all nine confirmed against real
|
|
6
|
+
* hardware. `ArmingMode` is both the const value-object (`ArmingMode.home`) and the union type of its
|
|
7
7
|
* values, so callers pass the named constant: `setMode(ArmingMode.home)`.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* The domain of `setMode` (cmd 1224) alone. The alarm-delay write (cmd 1255) carries its own mode integer
|
|
10
|
+
* on a separate wire and takes {@link AlarmDelayMode}, which stays NARROWER — evidence for one command is
|
|
11
|
+
* not evidence for the other, and 1255 still has no capture beyond its byte-captured three.
|
|
12
|
+
*
|
|
13
|
+
* A mode belongs here only once its write is confirmed against real hardware, because a fire-and-forget
|
|
14
|
+
* wire makes a wrong mode look exactly like success; `ARMING_MODE_WIRE` carries the per-value evidence.
|
|
15
|
+
* The union is also what `ARMING_MODE_WIRE` is keyed by, so the table cannot name a mode this does not.
|
|
14
16
|
*/
|
|
15
17
|
export declare const ArmingMode: {
|
|
16
18
|
/** Armed — full protection, nobody home (wire value 0). */
|
|
17
19
|
readonly away: "away";
|
|
18
20
|
/** Armed for occupancy — reduced/perimeter protection while home (wire value 1). */
|
|
19
21
|
readonly home: "home";
|
|
22
|
+
/** Scheduled — the station follows the timetable configured in the app (wire value 2). */
|
|
23
|
+
readonly schedule: "schedule";
|
|
24
|
+
/** Custom 1 — a user-defined posture configured in the app (wire value 3). */
|
|
25
|
+
readonly custom1: "custom1";
|
|
26
|
+
/** Custom 2 — a user-defined posture configured in the app (wire value 4). */
|
|
27
|
+
readonly custom2: "custom2";
|
|
28
|
+
/** Custom 3 — a user-defined posture configured in the app (wire value 5). */
|
|
29
|
+
readonly custom3: "custom3";
|
|
30
|
+
/** Off — the station's alarm system is switched off entirely (wire value 6). */
|
|
31
|
+
readonly off: "off";
|
|
32
|
+
/** Geofenced — the station follows the app's location-based rules (wire value 47). */
|
|
33
|
+
readonly geo: "geo";
|
|
20
34
|
/** Disarmed — no alarms; sensors still report state (wire value 63). */
|
|
21
35
|
readonly disarmed: "disarmed";
|
|
22
36
|
};
|
|
23
37
|
export type ArmingMode = (typeof ArmingMode)[keyof typeof ArmingMode];
|
|
38
|
+
/**
|
|
39
|
+
* The modes the alarm-delay write (cmd 1255) accepts a `mode_id` for — the three guard modes whose wire
|
|
40
|
+
* integer is byte-captured.
|
|
41
|
+
*
|
|
42
|
+
* A domain of its own, because the two mode integers ride different commands: `setMode` writes `mode_type`
|
|
43
|
+
* on cmd 1224, and cmd 1255 carries `mode_id`. Neither wire validates the integer, so each union IS its
|
|
44
|
+
* command's gate, and evidence for one is not evidence for the other. `custom1` is confirmed on 1224 only;
|
|
45
|
+
* 1255 has no capture carrying mode 3, and no known GET to read one back — the app's own replies
|
|
46
|
+
* `{count:0,data:null}` — so it is absent here. Narrower than {@link ArmingMode} by exactly that value.
|
|
47
|
+
*/
|
|
48
|
+
export declare const AlarmDelayMode: {
|
|
49
|
+
/** Armed — full protection, nobody home (wire value 0). */
|
|
50
|
+
readonly away: "away";
|
|
51
|
+
/** Armed for occupancy — reduced/perimeter protection while home (wire value 1). */
|
|
52
|
+
readonly home: "home";
|
|
53
|
+
/** Disarmed — no alarms; sensors still report state (wire value 63). */
|
|
54
|
+
readonly disarmed: "disarmed";
|
|
55
|
+
};
|
|
56
|
+
export type AlarmDelayMode = (typeof AlarmDelayMode)[keyof typeof AlarmDelayMode];
|
|
24
57
|
/**
|
|
25
58
|
* The P2P **feature-command ids** this arming capability drives. Capability-owned wire vocabulary
|
|
26
59
|
* (transport forwards `cmd.param` opaquely; full id→name catalog in the generated
|
|
@@ -34,10 +67,11 @@ export declare const ARMING_CMD: {
|
|
|
34
67
|
* mValue3:0, `payload:{mode_type:<int>, user_name:<string>}`.
|
|
35
68
|
*
|
|
36
69
|
* ⚠️ Only 3 of the 9 modes were exercised in that capture — `mode_type` 0 (away), 63 (disarmed), 1
|
|
37
|
-
* (home), all confirmed byte-exact
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
70
|
+
* (home), all confirmed byte-exact. Re-confirmed live 2026-08-05: each reported its own MODE_SWITCH push
|
|
71
|
+
* within ~5s of the write. The remaining six are live confirmations rather than captures — `custom1` 3
|
|
72
|
+
* first, then `schedule` 2, `custom2` 4, `custom3` 5, `off` 6 and `geo` 47 — each sent as this exact
|
|
73
|
+
* frame and each observed to bring MODE_SWITCH back, so all nine are settable. `ARMING_MODE_WIRE` has
|
|
74
|
+
* the per-value evidence and the dates.
|
|
41
75
|
*/
|
|
42
76
|
readonly SET_ARMING: 1224;
|
|
43
77
|
/**
|
|
@@ -138,11 +172,10 @@ export type ArmingActions = Surface<typeof ARMING_MEMBERS>;
|
|
|
138
172
|
*/
|
|
139
173
|
export declare const ARMING_MEMBERS: {
|
|
140
174
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* declaration.
|
|
175
|
+
* Read and write are the same nine modes, so `enumValues` is the whole domain: `writeDomain` falls back
|
|
176
|
+
* to it, and the derived setter, the refusal message and the offered argument all read from that one
|
|
177
|
+
* declaration. A member states an `args` entry only where the two sides DIFFER. See
|
|
178
|
+
* {@link ARMING_MODE_WIRE} for the per-value evidence.
|
|
146
179
|
*
|
|
147
180
|
* `armingCommand` may also throw synchronously (missing account identity) and `bindMembers` turns that
|
|
148
181
|
* into a rejection, so the builder stays plain.
|
|
@@ -162,11 +195,6 @@ export declare const ARMING_MEMBERS: {
|
|
|
162
195
|
readonly kind: "enum";
|
|
163
196
|
readonly enumValues: Record<number, string>;
|
|
164
197
|
readonly provenance: "verified";
|
|
165
|
-
readonly args: readonly [{
|
|
166
|
-
readonly name: "mode";
|
|
167
|
-
readonly kind: "enum";
|
|
168
|
-
readonly values: readonly number[];
|
|
169
|
-
}];
|
|
170
198
|
readonly description: string;
|
|
171
199
|
readonly observation: {
|
|
172
200
|
readonly event: "armingModeChanged";
|
|
@@ -187,15 +215,17 @@ export declare const ARMING_MEMBERS: {
|
|
|
187
215
|
* this mode — there is no known GET to fetch it automatically, and a wrong guess here can silently
|
|
188
216
|
* misconfigure which sensors arm/trigger for real.
|
|
189
217
|
*
|
|
190
|
-
* Takes {@link
|
|
191
|
-
* captured
|
|
192
|
-
* `
|
|
218
|
+
* Takes {@link AlarmDelayMode}, not {@link ArmingMode}: a delay is configurable only for a mode whose
|
|
219
|
+
* integer is captured on THIS command, and `custom1` is confirmed on cmd 1224 only. The frame carries
|
|
220
|
+
* that integer in `mode_id` with no runtime validation and no readback, so a mode outside this union
|
|
221
|
+
* would be the same unverified guess `setMode` refuses.
|
|
193
222
|
*/
|
|
194
|
-
readonly setAlarmDelayConfig: import("./members.js").MethodMember<(mode:
|
|
223
|
+
readonly setAlarmDelayConfig: import("./members.js").MethodMember<(mode: AlarmDelayMode, config: AlarmDelayConfig) => Promise<void>>;
|
|
195
224
|
};
|
|
196
225
|
/**
|
|
197
|
-
* `arming` — guard/arming mode. `armingMode` (see {@link ARMING_CMD.SET_ARMING}) has a verified
|
|
198
|
-
*
|
|
199
|
-
*
|
|
226
|
+
* `arming` — guard/arming mode. `armingMode` (see {@link ARMING_CMD.SET_ARMING}) has a verified read/write
|
|
227
|
+
* MECHANISM, and all 9 modes it reports are now confirmed as writes — the {@link ArmingMode} union is the
|
|
228
|
+
* whole set. See `ARMING_MODE_WIRE` for which three are byte-captured and which six are live-confirmed.
|
|
229
|
+
* The alarm-delay write (cmd 1255) is unaffected and keeps its narrower {@link AlarmDelayMode}.
|
|
200
230
|
*/
|
|
201
231
|
export declare const ARMING: CapabilityModule;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { CapabilityModule } from "./types.js";
|
|
2
|
+
import { type Surface } from "./members.js";
|
|
3
|
+
/**
|
|
4
|
+
* `display` — a eufy Smart Display's charge.
|
|
5
|
+
*
|
|
6
|
+
* One typed read, and the smallness is the finding rather than a gap. The captured unit (a T87A0,
|
|
7
|
+
* 2026-09-04) connects over secure MQTT with no `p2p_did`, so it speaks no P2P at all, and it reported
|
|
8
|
+
* six params in an id range no other line uses. Only one of them tells a caller something it could not
|
|
9
|
+
* get another way:
|
|
10
|
+
*
|
|
11
|
+
* - 8005 and 8006 restate the model's retail name and code, which `info` already answers from the
|
|
12
|
+
* curated registry and the cloud record. Their evidence IS that agreement, so a getter beside `info`
|
|
13
|
+
* would offer a caller a second spelling of what it just read. They are named in the display param
|
|
14
|
+
* dictionary, which is what makes them readable off `getProperties()`.
|
|
15
|
+
* - 8003 is version-shaped and nothing confirms what it means, so it is `unexposed`: in the schema with
|
|
16
|
+
* its type and its `guessed` label, reachable through `getProperty`, no typed getter.
|
|
17
|
+
*
|
|
18
|
+
* Nothing is writable. No capture pins a write for any display param, and an AIoT write is
|
|
19
|
+
* fire-and-forget, so a wrong frame to a device that acknowledges nothing looks exactly like success. A
|
|
20
|
+
* screen, a volume, an assistant: the device plainly has all three and reports none of them in anything
|
|
21
|
+
* captured, which means the reads have to arrive before a control can be honest about what it moves.
|
|
22
|
+
*
|
|
23
|
+
* @module model/capabilities/display
|
|
24
|
+
*/
|
|
25
|
+
/** Smart Display param ids this capability reads. The rest of the range is named in the dictionary. */
|
|
26
|
+
export declare const DISPLAY_PARAM: {
|
|
27
|
+
readonly BATTERY: 8001;
|
|
28
|
+
readonly SOFTWARE_VERSION: 8003;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The `display` reads. Read-only; see the module note for why there is no write.
|
|
32
|
+
*
|
|
33
|
+
* Exported but NOT published: the entries state their wire ids and what each claim rests on, which the
|
|
34
|
+
* reference site does not carry.
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
export declare const DISPLAY_MEMBERS: {
|
|
38
|
+
/**
|
|
39
|
+
* Battery level, 0-100.
|
|
40
|
+
*
|
|
41
|
+
* **On this capability rather than on `battery`, and that is the line partition doing its job.** The
|
|
42
|
+
* security-line `battery` capability reads param 1101 and a Smart Display's charge is 8001 in its own
|
|
43
|
+
* id space — two wires that happen to mean the same thing. One capability reading both would be a claim
|
|
44
|
+
* that the two ecosystems share a param space, so a consumer reads a display's charge through
|
|
45
|
+
* `dev.display()` and a camera's through `dev.battery()`.
|
|
46
|
+
*
|
|
47
|
+
* `verified` rather than `mega`: the id is in the device's cloud record, but the NAME came from the
|
|
48
|
+
* maintainer's own knowledge of the hardware rather than from the cloud data-point list, and `"100"`
|
|
49
|
+
* fits brightness, volume or charge equally.
|
|
50
|
+
*
|
|
51
|
+
* The scale is `percent` on the reading itself, not on convention alone: a full charge reads `255` on a
|
|
52
|
+
* 0-255 scale and `1000` on a 0-1000 one, so `"100"` on a charged unit is positive evidence for 0-100
|
|
53
|
+
* rather than merely consistent with it. What nobody has done is watch it MOVE, which is why a value
|
|
54
|
+
* frozen at 100 would not yet be distinguishable from a healthy one.
|
|
55
|
+
*/
|
|
56
|
+
readonly battery: {
|
|
57
|
+
readonly param: 8001;
|
|
58
|
+
readonly type: "number";
|
|
59
|
+
readonly unit: "%";
|
|
60
|
+
readonly kind: "percent";
|
|
61
|
+
readonly provenance: "verified";
|
|
62
|
+
readonly description: "Battery level, 0-100 (param 8001).";
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Version-shaped, and that shape is the whole of the evidence — hence `guessed`, and hence no typed
|
|
66
|
+
* getter: a caller reading this off a bound object cannot see the label.
|
|
67
|
+
*
|
|
68
|
+
* `unexposed` rather than absent, so the schema still carries its type and that label and
|
|
69
|
+
* `getProperty("softwareVersion")` still answers. `dev.info()?.firmwareVersion` is the field to trust
|
|
70
|
+
* where the cloud record carries one; on this display it does not, which is the only reason 8003 is
|
|
71
|
+
* named at all.
|
|
72
|
+
*/
|
|
73
|
+
readonly softwareVersion: {
|
|
74
|
+
readonly param: 8003;
|
|
75
|
+
readonly type: "string";
|
|
76
|
+
readonly kind: "text";
|
|
77
|
+
readonly provenance: "guessed";
|
|
78
|
+
readonly unexposed: true;
|
|
79
|
+
readonly description: "Version-shaped string (param 8003), meaning unconfirmed — prefer `info.firmwareVersion`.";
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
/** `display` — a eufy Smart Display (T87Ax). Read-only; no display write is captured. */
|
|
83
|
+
export declare const DISPLAY: CapabilityModule;
|
|
84
|
+
/** Bound Smart Display reads — the object returned by `dev.display()`. Read-only. */
|
|
85
|
+
export type DisplayActions = Surface<typeof DISPLAY_MEMBERS>;
|
|
@@ -40,6 +40,7 @@ import type { KeypadActions } from "./keypad.js";
|
|
|
40
40
|
import type { RtspActions } from "./rtsp.js";
|
|
41
41
|
import type { VacuumCleanActions } from "./vacuum-clean.js";
|
|
42
42
|
import type { VacuumDockActions } from "./vacuum-dock.js";
|
|
43
|
+
import type { DisplayActions } from "./display.js";
|
|
43
44
|
import type { SuctionActions } from "./suction.js";
|
|
44
45
|
import type { LocateActions } from "./locate.js";
|
|
45
46
|
import type { DeviceInfo } from "./info.js";
|
|
@@ -74,10 +75,12 @@ export declare function mergeProperties(caps: Capability[], ctx?: AvailabilityCo
|
|
|
74
75
|
* - a `modelHints` regex matches the model/category/name haystack,
|
|
75
76
|
* - `codecs` includes `codec`,
|
|
76
77
|
* - `detect(rec, codec)` returns true.
|
|
77
|
-
*
|
|
78
|
+
*
|
|
79
|
+
* An absent `codec` belongs to no line, so only the line-agnostic capabilities can match — the truthful
|
|
80
|
+
* answer for a device outside the eufy families entirely. Never throws. Returns a de-duplicated array.
|
|
78
81
|
* @internal
|
|
79
82
|
*/
|
|
80
|
-
export declare function detectCapabilities(rec: CloudRecord, codec
|
|
83
|
+
export declare function detectCapabilities(rec: CloudRecord, codec?: Codec): Capability[];
|
|
81
84
|
/**
|
|
82
85
|
* The baseline capabilities a codec grants every device of that family — derived from the modules
|
|
83
86
|
* that declare the codec in their {@link import("./types").DetectionSpec} `codecs`. Each capability
|
|
@@ -385,7 +388,7 @@ export interface DeviceActionMap {
|
|
|
385
388
|
lock: LockActions;
|
|
386
389
|
/** Siren: reads `active`, `volume`, `alarmDuration`, `doNotDisturb`; writes `setVolume`, `setAlarmDuration`, `test`, `stop` (config setters present when the param is reported). No direct "sound the alarm" wire — a real alarm is driven by the `arming` system; `test` is the on-demand trigger. */
|
|
387
390
|
siren: SirenActions;
|
|
388
|
-
/** Guard mode: `setMode(ArmingMode)` + `setAlarmDelayConfig(mode, config)`. Of the
|
|
391
|
+
/** Guard mode: `setMode(ArmingMode)` + `setAlarmDelayConfig(mode, config)`. Of the 9 modes a station reports, only `away`/`home`/`custom1`/`disarmed` are confirmed as writes (`ArmingMode`); the alarm-delay write takes the narrower byte-captured `AlarmDelayMode`. */
|
|
389
392
|
arming: ArmingActions;
|
|
390
393
|
/** Doorbell: `playQuickResponse(voiceId)` (the canned voice replies). */
|
|
391
394
|
doorbell: DoorbellActions;
|
|
@@ -411,6 +414,8 @@ export interface DeviceActionMap {
|
|
|
411
414
|
suction: SuctionActions;
|
|
412
415
|
/** RoboVac locate (find-robot beep): `locating`; `locate(on?)`. */
|
|
413
416
|
locate: LocateActions;
|
|
417
|
+
/** Smart Display (read-only): `battery`. No display write is captured. */
|
|
418
|
+
display: DisplayActions;
|
|
414
419
|
/** Identity metadata (read-only): `{ manufacturer, model, serialNumber, name, deviceType?, firmwareVersion?, hardwareVersion? }`. */
|
|
415
420
|
info: DeviceInfo;
|
|
416
421
|
}
|
|
@@ -529,6 +534,7 @@ export { RTSP_MEMBERS } from "./rtsp.js";
|
|
|
529
534
|
export { SIREN_MEMBERS } from "./siren.js";
|
|
530
535
|
export { SMART_LIGHT_MEMBERS } from "./smart-light.js";
|
|
531
536
|
export { SMOKE_MEMBERS } from "./smoke.js";
|
|
537
|
+
export { DISPLAY_MEMBERS, type DisplayActions } from "./display.js";
|
|
532
538
|
export { SUCTION_MEMBERS } from "./suction.js";
|
|
533
539
|
export { VACUUM_CLEAN_MEMBERS } from "./vacuum-clean.js";
|
|
534
540
|
export type { DeviceInfo } from "./info.js";
|
|
@@ -550,12 +556,12 @@ export { HubAlarmTone, type HubAlarmToneValue } from "./siren.js";
|
|
|
550
556
|
* RoboVac activity and clean type are the declared returns of the public `dev.vacuumClean()` getters,
|
|
551
557
|
* so both unions are published.
|
|
552
558
|
*/
|
|
553
|
-
export type { VacuumActivity, VacuumCleanType, CarpetStrategy, CleanExtent } from "./vacuum-clean.js";
|
|
559
|
+
export type { VacuumActivity, VacuumCleanType, CarpetStrategy, CleanExtent, VacuumRoomTarget, VacuumZoneTarget, } from "./vacuum-clean.js";
|
|
554
560
|
/** The lists those unions are taken from — published because each union names its own. */
|
|
555
561
|
export { VACUUM_ACTIVITIES, VACUUM_CLEAN_TYPES, CARPET_STRATEGIES, CLEAN_EXTENTS, MOP_LEVELS } from "./vacuum-clean.js";
|
|
556
562
|
export { SuctionLevel, suctionLevelName, type SuctionLevelValue } from "./suction.js";
|
|
557
563
|
export type { PtzPresetActions, ZoomRegion, PtzPreset, PtzPresetImage } from "./ptz.js";
|
|
558
|
-
export { ArmingMode } from "./arming.js";
|
|
564
|
+
export { AlarmDelayMode, ArmingMode } from "./arming.js";
|
|
559
565
|
export type { AlarmDelayConfig, AlarmDelayCountdown, AlarmDelayDeviceAction, AlarmDelaySeconds } from "./arming.js";
|
|
560
566
|
export { PtzDirection } from "./ptz.js";
|
|
561
567
|
export { AiDetectType, encodeAiDetectType, decodeAiDetectType, type AiDetectFlags } from "./motion.js";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anker **Solix** capability surface. Solix is a separate ecosystem — its own Anker account, backend
|
|
3
|
+
* and product catalog — so it keeps its own capability id union rather than joining eufy's `Capability`
|
|
4
|
+
* / `Codec` unions, and detection is by Anker catalog CATEGORY + product-code prefix (see
|
|
5
|
+
* {@link detectSolixCapabilities}) rather than eufy param ids.
|
|
6
|
+
*
|
|
7
|
+
* What it shares is the `members` engine: the one feature with a readable wire declares ONE `members`
|
|
8
|
+
* table, and its property schema, evidence gate and typed surface all derive from it through
|
|
9
|
+
* `members.ts` (`bindMembers` / `Surface`), exactly as a eufy capability does.
|
|
10
|
+
*
|
|
11
|
+
* @module model/capabilities/solix
|
|
12
|
+
*/
|
|
13
|
+
import type { Surface } from "./members.js";
|
|
14
|
+
/** Every capability a Solix device may carry. Solix's OWN union (not eufy's `Capability`). */
|
|
15
|
+
export type SolixCapability = "identity" | "firmware" | "connectivity" | "energyMeter" | "battery" | "solarInput" | "acOutput" | "evCharger" | "charger" | "cooler";
|
|
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.
|
|
21
|
+
*
|
|
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.
|
|
27
|
+
*
|
|
28
|
+
* @internal — the declaration `SolixEnergyMeterReads` derives from; exported (like the eufy `*_MEMBERS`
|
|
29
|
+
* tables) so it is a known symbol, but excluded from the rendered API reference.
|
|
30
|
+
*/
|
|
31
|
+
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
|
+
readonly meterVoltageL1: {
|
|
38
|
+
readonly param: 172;
|
|
39
|
+
readonly type: "number";
|
|
40
|
+
readonly kind: "scalar";
|
|
41
|
+
readonly unit: "V";
|
|
42
|
+
readonly provenance: "verified";
|
|
43
|
+
readonly description: "Meter line-1 voltage (V) — ff09 tag 0xAC, confirmed against a live single-phase frame.";
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
/** Bound `energyMeter` reads (the members-derived half of `dev.energyMeter()`). Read-only. */
|
|
47
|
+
export type SolixEnergyMeterReads = Surface<typeof SOLIX_ENERGY_METER_MEMBERS>;
|
|
48
|
+
/**
|
|
49
|
+
* The capabilities each Anker catalog category implies. Category is a detection SIGNAL (like eufy's
|
|
50
|
+
* `deviceTypes`), not the model's identity — a device still resolves `energyMeter` from its product code
|
|
51
|
+
* even though its category is "Accessory", which is why that category maps to nothing on its own.
|
|
52
|
+
* Unlisted categories contribute nothing here.
|
|
53
|
+
*/
|
|
54
|
+
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). */
|
|
56
|
+
export declare const SOLIX_METER_MODELS: readonly string[];
|
|
57
|
+
/**
|
|
58
|
+
* Product-code prefixes for the grid-tie Solarbank / home-battery family (detects `battery` +
|
|
59
|
+
* `solarInput` regardless of category): A1790 = Solarbank E1600 gen-1, A17C* = Solarbank 2 / 3.
|
|
60
|
+
*/
|
|
61
|
+
export declare const SOLARBANK_MODELS: readonly string[];
|
|
62
|
+
/** The minimum device shape {@link detectSolixCapabilities} reads. */
|
|
63
|
+
export interface SolixDetectionInput {
|
|
64
|
+
product_code: string;
|
|
65
|
+
device_sw_version?: string;
|
|
66
|
+
wifi_online?: boolean;
|
|
67
|
+
wifi_name?: string;
|
|
68
|
+
rssi?: string | number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve a Solix device's capability set from its record fields, catalog category, and product-code
|
|
72
|
+
* prefix — the Solix analogue of eufy's `detectCapabilities`, kept Solix-scoped so eufy detection is
|
|
73
|
+
* untouched. `identity` is universal; the rest are OR-ed evidence.
|
|
74
|
+
*/
|
|
75
|
+
export declare function detectSolixCapabilities(rec: SolixDetectionInput, category?: string): Set<SolixCapability>;
|
|
@@ -41,12 +41,19 @@ export interface DetectionSpec {
|
|
|
41
41
|
*
|
|
42
42
|
* eufy ships several ecosystems that share a cloud account and nothing else: `security` (cameras,
|
|
43
43
|
* stations, locks, sensors — P2P plus the security-scoped broker), `life` (the T8L0x smart-lighting
|
|
44
|
-
* line — its own credential and its own DP wire),
|
|
44
|
+
* line — its own credential and its own DP wire), `clean` (robot vacuums — Tuya data points) and
|
|
45
|
+
* `display` (the T87Ax Smart Display — secure MQTT, never P2P, its own 8001-8006 param space).
|
|
45
46
|
* They overlap in retail vocabulary but share no wire, no param space and no semantics.
|
|
46
47
|
*
|
|
48
|
+
* `display` is a line of its own for the second of those reasons rather than the first: without it,
|
|
49
|
+
* every security capability detected by a NAME regex is attachable to a Smart Display — measured at six,
|
|
50
|
+
* on a device that can answer for none of them because it speaks no P2P at all. A line holding one
|
|
51
|
+
* capability still buys that, which is why the count is not the measure of whether a line is worth
|
|
52
|
+
* declaring.
|
|
53
|
+
*
|
|
47
54
|
* `any` is for the handful of capabilities that are genuinely line-independent (device identity).
|
|
48
55
|
*/
|
|
49
|
-
export type ProductLine = "security" | "life" | "clean" | "print" | "any";
|
|
56
|
+
export type ProductLine = "security" | "life" | "clean" | "print" | "display" | "any";
|
|
50
57
|
/**
|
|
51
58
|
* A structural subset of a P2P frame. Deliberately NOT `import`ed from `p2p/*` — keeping it
|
|
52
59
|
* structural avoids a model→p2p cycle, and the real `P2PFrame` is assignable to it. It is the
|
|
@@ -162,8 +169,13 @@ export interface DecodedState {
|
|
|
162
169
|
* the manifest path and the command path.
|
|
163
170
|
*/
|
|
164
171
|
export interface AvailabilityContext {
|
|
165
|
-
/**
|
|
166
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Resolved codec/family. Absent for a device outside the eufy device model entirely — the codecs are
|
|
174
|
+
* the eufy transport families, so an ecosystem with its own backend has no truthful value here and
|
|
175
|
+
* says so by omission rather than borrowing another family's. Every gate that reads it compares
|
|
176
|
+
* against a specific codec, so an absent one matches none.
|
|
177
|
+
*/
|
|
178
|
+
codec?: Codec;
|
|
167
179
|
/** eufy DeviceType, when known. */
|
|
168
180
|
deviceType?: number;
|
|
169
181
|
/** Model / T-code, when known. */
|
|
@@ -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/index.d.ts
CHANGED
|
@@ -30,3 +30,6 @@ 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";
|
|
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";
|
|
@@ -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,20 @@
|
|
|
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
|
+
export declare function buildModelIndex(categories: SolixProductCategory[]): Map<string, {
|
|
18
|
+
name: string;
|
|
19
|
+
category: string;
|
|
20
|
+
}>;
|