@mega-yfue/eufy-sdk 0.0.4 → 0.1.0-beta.0
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/README.md +31 -12
- package/dist/client/device-registry.d.ts +343 -0
- package/dist/client/eufy-mega.d.ts +872 -0
- package/dist/client/index.d.ts +1 -6
- package/dist/client/map-channels.d.ts +22 -0
- package/dist/client/types.d.ts +383 -0
- package/dist/core/contracts.d.ts +898 -0
- package/dist/core/crypto.d.ts +98 -0
- package/dist/core/index.d.ts +9 -7
- package/dist/core/logger.d.ts +53 -0
- package/dist/core/lz4-block.d.ts +35 -0
- package/dist/core/raw-dp-hex.d.ts +32 -0
- package/dist/core/raw-dp-writer.d.ts +83 -0
- package/dist/core/store.d.ts +43 -0
- package/dist/core/types.d.ts +169 -0
- package/dist/core/util.d.ts +78 -0
- package/dist/index.d.ts +4 -7
- package/dist/index.js +25526 -14
- package/dist/index.js.map +7 -1
- package/dist/model/capabilities/access.d.ts +127 -0
- package/dist/model/capabilities/arming.d.ts +201 -0
- package/dist/model/capabilities/audio.d.ts +154 -0
- package/dist/model/capabilities/battery.d.ts +363 -0
- package/dist/model/capabilities/camera.d.ts +564 -0
- package/dist/model/capabilities/co.d.ts +40 -0
- package/dist/model/capabilities/contact.d.ts +133 -0
- package/dist/model/capabilities/doorbell.d.ts +345 -0
- package/dist/model/capabilities/dp-catalog.d.ts +38 -0
- package/dist/model/capabilities/index.d.ts +561 -0
- package/dist/model/capabilities/info.d.ts +28 -0
- package/dist/model/capabilities/keypad.d.ts +61 -0
- package/dist/model/capabilities/leak.d.ts +43 -0
- package/dist/model/capabilities/light.d.ts +174 -0
- package/dist/model/capabilities/locate.d.ts +63 -0
- package/dist/model/capabilities/lock.d.ts +242 -0
- package/dist/model/capabilities/manifest.d.ts +107 -0
- package/dist/model/capabilities/members.d.ts +647 -0
- package/dist/model/capabilities/motion.d.ts +377 -0
- package/dist/model/capabilities/person-detection.d.ts +8 -0
- package/dist/model/capabilities/ptz.d.ts +289 -0
- package/dist/model/capabilities/rtsp.d.ts +221 -0
- package/dist/model/capabilities/siren.d.ts +218 -0
- package/dist/model/capabilities/smart-light.d.ts +172 -0
- package/dist/model/capabilities/smoke.d.ts +40 -0
- package/dist/model/capabilities/snapshot.d.ts +6 -0
- package/dist/model/capabilities/storage.d.ts +11 -0
- package/dist/model/capabilities/suction.d.ts +104 -0
- package/dist/model/capabilities/types.d.ts +484 -0
- package/dist/model/capabilities/vacuum-clean.d.ts +1946 -0
- package/dist/model/capabilities/vacuum-dock.d.ts +208 -0
- package/dist/model/capabilities/video.d.ts +6 -0
- package/dist/model/classify.d.ts +78 -0
- package/dist/model/clean-record-detail.d.ts +65 -0
- package/dist/model/clean-records.d.ts +69 -0
- package/dist/model/device-family.d.ts +73 -0
- package/dist/model/device-types.d.ts +123 -0
- package/dist/model/device.d.ts +265 -0
- package/dist/model/index.d.ts +29 -4
- package/dist/model/infer.d.ts +23 -0
- package/dist/model/inspect.d.ts +61 -0
- package/dist/model/life-params.d.ts +21 -0
- package/dist/model/map-pixels.d.ts +70 -0
- package/dist/model/param-dictionary.d.ts +28 -0
- package/dist/model/param-namespace.d.ts +21 -0
- package/dist/model/proto-read.d.ts +53 -0
- package/dist/model/push-events.d.ts +147 -0
- package/dist/model/registry.d.ts +54 -0
- package/dist/model/types.d.ts +301 -0
- package/dist/model/vacuum-map-store.d.ts +92 -0
- package/dist/model/vacuum-map.d.ts +286 -0
- package/dist/model/vacuum-scenes.d.ts +76 -0
- package/dist/model/vacuum-schedules.d.ts +85 -0
- package/dist/transport/dp-preset.d.ts +102 -0
- package/dist/transport/ff09.d.ts +444 -0
- package/dist/transport/ffmpeg.d.ts +86 -0
- package/dist/transport/http/decodeImageV1.d.ts +20 -0
- package/dist/transport/http/decodeImageV2.d.ts +19 -0
- package/dist/transport/http/index.d.ts +5 -0
- package/dist/transport/http/light-catalog.d.ts +62 -0
- package/dist/transport/http/media-download.d.ts +14 -0
- package/dist/transport/http/mega-client.d.ts +514 -0
- package/dist/transport/http/phone-model.d.ts +21 -0
- package/dist/transport/index.d.ts +10 -7
- package/dist/transport/mqtt/app-client-id.d.ts +16 -0
- package/dist/transport/mqtt/availability.d.ts +14 -0
- package/dist/transport/mqtt/bare-ip-tls.d.ts +46 -0
- package/dist/transport/mqtt/biz-stream.d.ts +98 -0
- package/dist/transport/mqtt/broker-discovery.d.ts +55 -0
- package/dist/transport/mqtt/clean-codec.d.ts +14 -0
- package/dist/transport/mqtt/command-router.d.ts +285 -0
- package/dist/transport/mqtt/dp-codec.d.ts +58 -0
- package/dist/transport/mqtt/dp-color.d.ts +14 -0
- package/dist/transport/mqtt/engine.d.ts +16 -0
- package/dist/transport/mqtt/index.d.ts +5 -0
- package/dist/transport/mqtt/secure-mqtt.d.ts +107 -0
- package/dist/transport/mqtt/topics.d.ts +80 -0
- package/dist/transport/p2p/adts.d.ts +91 -0
- package/dist/transport/p2p/annexb.d.ts +124 -0
- package/dist/transport/p2p/codec.d.ts +166 -0
- package/dist/transport/p2p/command-router.d.ts +655 -0
- package/dist/transport/p2p/commands.d.ts +550 -0
- package/dist/transport/p2p/envelope.d.ts +46 -0
- package/dist/transport/p2p/fmp4.d.ts +89 -0
- package/dist/transport/p2p/fragment-recording.d.ts +33 -0
- package/dist/transport/p2p/index.d.ts +13 -0
- package/dist/transport/p2p/lan-ip.d.ts +25 -0
- package/dist/transport/p2p/live-stream.d.ts +223 -0
- package/dist/transport/p2p/live-trace.d.ts +123 -0
- package/dist/transport/p2p/media.d.ts +105 -0
- package/dist/transport/p2p/p2p-session.d.ts +620 -0
- package/dist/transport/p2p/readable-egress.d.ts +27 -0
- package/dist/transport/p2p/session-manager.d.ts +154 -0
- package/dist/transport/p2p/shared-live-source.d.ts +431 -0
- package/dist/transport/p2p/talkback.d.ts +187 -0
- package/dist/transport/p2p/video.d.ts +150 -0
- package/dist/transport/p2p/write-commands.d.ts +21 -0
- package/dist/transport/protobuf.d.ts +5 -0
- package/dist/transport/push/fcm.d.ts +23 -0
- package/dist/transport/push/index.d.ts +6 -0
- package/dist/transport/push/message-tags.d.ts +26 -0
- package/dist/transport/push/parser.d.ts +27 -0
- package/dist/transport/push/proto.d.ts +11 -0
- package/dist/transport/push/push-client.d.ts +64 -0
- package/dist/transport/push/store.d.ts +23 -0
- package/dist/transport/push/types.d.ts +180 -0
- package/dist/transport/raw-dp.d.ts +6 -0
- package/dist/transport/stored-image-cache.d.ts +23 -0
- package/dist/transport/tuya/account.d.ts +44 -0
- package/dist/transport/tuya/client.d.ts +89 -0
- package/dist/transport/tuya/command-router.d.ts +79 -0
- package/dist/transport/tuya/dp-codec.d.ts +56 -0
- package/dist/transport/tuya/index.d.ts +28 -0
- package/dist/transport/tuya/request.d.ts +157 -0
- package/dist/transport/tuya/sign.d.ts +64 -0
- package/package.json +12 -13
- package/dist/client/index.js +0 -2
- package/dist/client/index.js.map +0 -1
- package/dist/core/index.js +0 -2
- package/dist/core/index.js.map +0 -1
- package/dist/model/index.js +0 -2
- package/dist/model/index.js.map +0 -1
- package/dist/transport/index.js +0 -2
- package/dist/transport/index.js.map +0 -1
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ff09` frame encoder — the AES-128-CBC framed control protocol shared by every eufy device that
|
|
3
|
+
* takes a lock-style on/off actuation command (currently: the T8531 video smart lock and the T85D0
|
|
4
|
+
* garage door; likely other actuators later — see below).
|
|
5
|
+
*
|
|
6
|
+
* The command bytes are a self-contained `ff09…` frame, NOT a plain param write, and BOTH transports
|
|
7
|
+
* carry the exact same frame:
|
|
8
|
+
* - **P2P** (`transport/p2p/command-router.ts`, T8531): wrapped in a `SET_PAYLOAD` (1350) envelope
|
|
9
|
+
* whose inner `cmd` is `TRANSFER_PAYLOAD` (1940) and `payload` = `{apiCommand:6018,
|
|
10
|
+
* lock_payload:"ff09…", seq_num, time}`.
|
|
11
|
+
* - **MQTT** (`transport/mqtt/command-router.ts`, standalone/garage families): base64 of the identical
|
|
12
|
+
* frame under the `trans` field.
|
|
13
|
+
*
|
|
14
|
+
* So cracking the frame solves both — this module builds ONLY the frame, nothing about either
|
|
15
|
+
* envelope, and sits at the transport root (like `ffmpeg.ts`) specifically so neither transport
|
|
16
|
+
* imports the other for it. Named after the wire frame, not the capability: the two current callers
|
|
17
|
+
* both happen to be locks, but nothing here is lock-specific beyond the {@link LOCK_COMMAND_CODE}
|
|
18
|
+
* opcode value itself — a future actuator (e.g. a curtain) reusing this wire would just be a new
|
|
19
|
+
* opcode constant, not a new encoder.
|
|
20
|
+
*
|
|
21
|
+
* ## Wire (verified byte-exact against captured T8531 lock+unlock frames)
|
|
22
|
+
*
|
|
23
|
+
* **Outer frame** (`lockPayload`, hex):
|
|
24
|
+
* ```
|
|
25
|
+
* ff 09 | size(u16 LE) | 03(versionCode) 00(reserved) 02(dataType) | cmdEnc(u16 BE) | ct… | xorHash(1)
|
|
26
|
+
* ```
|
|
27
|
+
* `size` = total frame length incl. header + xor. `cmdEnc` = `0x4000 | commandCode` (bit 0x4000 =
|
|
28
|
+
* "encrypted payload"). `xorHash` = XOR of every preceding byte.
|
|
29
|
+
*
|
|
30
|
+
* **Inner plaintext** — an incrementing-separator TLV (`0xA1, 0xA2, …`, each `sep | len | bytes`),
|
|
31
|
+
* then **zero-padded to a 16-byte boundary** before AES (the app pads the buffer, so the real PKCS#7
|
|
32
|
+
* block AES adds is a full 0x10 block):
|
|
33
|
+
* ```
|
|
34
|
+
* A1 04 <time u32 LE> — freshness stamp
|
|
35
|
+
* A2 <len> <admin_user_id ASCII> — the lock owner's id (40 hex chars)
|
|
36
|
+
* A3 01 <lockByte> — 0 = lock, 1 = unlock
|
|
37
|
+
* A4 <len> <username ASCII> — the acting user's display name (email local-part)
|
|
38
|
+
* A5 <len> <short_user_id bytes> — the acting member's short id (hex→bytes, e.g. "0003")
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* **Cipher**: AES-128-CBC + PKCS#7.
|
|
42
|
+
* - key = `ASCII(admin_user_id[-12:])` (12 B) ‖ `uint32 BE(time)` (4 B) = 16 B.
|
|
43
|
+
* - iv = `ASCII(deviceSn)` zero-padded to 16 B (a 16-char SN is used verbatim).
|
|
44
|
+
* - `time` is the nonce that makes each command's key differ; it is emitted CLEARTEXT in the
|
|
45
|
+
* envelope `time` field so the lock can re-derive the key. The app computes it as
|
|
46
|
+
* `floor(unixSec) | floor(random*100)` — the low bits are randomized. We reproduce that:
|
|
47
|
+
* `keyTime = unixSec | nonce`, and the TLV `A1` stamp carries the un-noised `unixSec`.
|
|
48
|
+
*
|
|
49
|
+
* ## Settings GET/SET (T85D0 garage/lock auto-lock, 2026-07-16 capture — same cipher/frame family)
|
|
50
|
+
*
|
|
51
|
+
* Two more `apiCommand`/`commandCode` pairs on the SAME outer frame + cipher as above, verified
|
|
52
|
+
* byte-exact against a live GET query + a live GET response + two live SET writes (off→on) on a
|
|
53
|
+
* T85D0:
|
|
54
|
+
* - **GET** (`LOCK_API_COMMAND.GET_SETTINGS` 6016 / `LOCK_COMMAND_CODE.GET_SETTINGS` 53/0x35,
|
|
55
|
+
* {@link buildFf09QueryFrame}): plaintext is JUST `[A1 time][A2 adminUserId]` — no user-attribution
|
|
56
|
+
* fields at all (narrower than even `omitUserFields`, which still keeps A3).
|
|
57
|
+
* - **SET** (`LOCK_API_COMMAND.SET_SETTINGS` 6015 / `LOCK_COMMAND_CODE.SET_SETTINGS` 52/0x34,
|
|
58
|
+
* {@link buildFf09SettingsFrame}): `[A1 time][A2 adminUserId][A3 1B][A4 1B enable][A5 2B LE delay-
|
|
59
|
+
* seconds][A6 1B][A7 2B][A8 2B][A9 1B]`. `A3` is the same **setting-type selector** the Rain Mode
|
|
60
|
+
* compact write uses (see below) — `0x00` selects auto-lock. `A4`-`A8` are now FULLY CONFIRMED
|
|
61
|
+
* against the app's own JS (`smartLockSetParamsDataParse`'s `case 0`, carved from a live `/proc/mem`
|
|
62
|
+
* dump, 2026-07-18 — not just a capture-derived guess): `A4`=`isAutoLock`, `A5`=`autoLockTime`
|
|
63
|
+
* (seconds, LE u16), `A6`=`isSchedule` (bool), `A7`=`scheduleStartTime`, `A8`=`scheduleEndTime`.
|
|
64
|
+
* `A7`/`A8` are each `[hourByte, minuteByte]` (the app encodes them as `hour.toString(16)‖
|
|
65
|
+
* minute.toString(16)`, NOT a packed LE u16) — we still treat them as an opaque 2-byte passthrough
|
|
66
|
+
* (read from a prior GET, written back unchanged), which stays correct either way since we never
|
|
67
|
+
* interpret their contents, only preserve them. **`A9` is UNRESOLVED, not "confirmed reserved" —
|
|
68
|
+
* the app's own `case 0` builder writes exactly 5 fields (`A4`-`A8`) and produces no 6th field at
|
|
69
|
+
* all.** The original capture this encoder was built from showed a 6th real tagged byte
|
|
70
|
+
* (`0xA9`, value `0x00`) after `A8`; two independent copies of the app's JS (including a newer
|
|
71
|
+
* 15-case build variant) agree there's no `A9` in `case 0`. This is a genuine, unresolved
|
|
72
|
+
* discrepancy between the captured wire bytes and the decompiled source — not yet re-verified with
|
|
73
|
+
* a fresh capture. Left as-is in the shipped encoder (still sends `A9=0x00`) since changing it
|
|
74
|
+
* without a fresh capture risks breaking a live-verified write over an unconfirmed hypothesis; the
|
|
75
|
+
* device has never rejected the extra field in any live test.
|
|
76
|
+
* - **The device's `/res` reply to a GET** is NOT a normal command ack: no `mChannel`/`mValue3`/
|
|
77
|
+
* `apiCommand`/`seq_num`, just `{cmd:1940, payload:{dev_sn, lock_payload, time}}` where `time` is a
|
|
78
|
+
* **hex string** (not decimal) that equals the keyTime of the GET that triggered it — matched back
|
|
79
|
+
* to the query to survive interleaved traffic. Its `cmdEnc` has an extra flag bit set vs. a request
|
|
80
|
+
* (`0x48xx` not `0x40xx`) but decodes with the exact same key/iv derivation. Decrypted plaintext:
|
|
81
|
+
* a leading `0x00` status byte, then TLV fields using the same `sep|len|bytes` encoding but the
|
|
82
|
+
* response's OWN independent tag numbering (does not line up positionally with the GET/SET request
|
|
83
|
+
* TLVs) — our own capture only ever showed `a1..ad` (13 fields, tags `0xa1`-`0xad`), but this is NOT
|
|
84
|
+
* the auto-lock write's own reply scoped to auto-lock: **a single `GET_SETTINGS` reply is a FLAT
|
|
85
|
+
* DUMP of the device's ENTIRE settings state**, one field per tag, covering every setting in the
|
|
86
|
+
* `A3`/setting-type enum below (not just the one being written) — confirmed against
|
|
87
|
+
* the app's own `smartLockGetParamsDataParse` (2026-07-18, same JS dump as the case-0 finding
|
|
88
|
+
* above), which explicit-tag-reads (not sequentially, `getByteParam(<tag>)`/`getSecondParam(<tag>)`/
|
|
89
|
+
* `getTimeParam(<tag>)`) THIRTY-FOUR fields, tags `0xa1` through `0xc2`:
|
|
90
|
+
*
|
|
91
|
+
* | tag | field | tag | field |
|
|
92
|
+
* |------|-------------------------------------|------|-------------------------------------|
|
|
93
|
+
* | 0xa1 | `isAutoLock` **(current enable state — use THIS, not `A4`, to read it back)** | 0xb3 | *(read, unused in this build)* |
|
|
94
|
+
* | 0xa2 | `autoLockTime` (= our `A5`/delay readback) | 0xb4 | *(read, unused in this build)* |
|
|
95
|
+
* | 0xa3 | `isSchedule` (= our `A6`) | 0xb5 | `isPowerSavingMode` |
|
|
96
|
+
* | 0xa4 | `scheduleStartTime` (= our `A7` readback) | 0xb6 | `powerSavingModeStartTime` |
|
|
97
|
+
* | 0xa5 | `scheduleEndTime` (= our `A8` readback) | 0xb7 | `powerSavingModeEndTime` |
|
|
98
|
+
* | 0xa6 | `isOneTouchLock` (setting-type 1) | 0xb8 | `keepAliveTime` |
|
|
99
|
+
* | 0xa7 | `isScramblePasscode` (setting-type 3) | 0xb9 | `isOneTouchRearLock` (setting-type 11) |
|
|
100
|
+
* | 0xa8 | `isWrongTryProtect` (setting-type 2) | 0xba | `multiFunctionBtnSinglePressMode` (12) |
|
|
101
|
+
* | 0xa9 | `wrongTryTime` | 0xbb | `multiFunctionBtnDoublePressMode` |
|
|
102
|
+
* | 0xaa | `lockDownTime` | 0xbc | `multiFunctionBtnLongPressMode` |
|
|
103
|
+
* | 0xab | `lockVolume` (setting-type 4) | 0xbd | `isSoundNotDisturbSchedule` |
|
|
104
|
+
* | 0xac | `wifiStatus` (setting-type 5) | 0xbe | `soundNotDisturbScheduleStart` |
|
|
105
|
+
* | 0xad | `isEnableLog` (setting-type 6) | 0xbf | `soundNotDisturbScheduleEnd` |
|
|
106
|
+
* | 0xae | `isRainMode` (setting-type 7) | 0xc0 | `faceEnabled` (setting-type 13, default 1) |
|
|
107
|
+
* | 0xaf | `isPassageMode` (setting-type 8) | 0xc1 | `faceWakeType` (default 0) |
|
|
108
|
+
* | 0xb0 | `passageModeStartTime` | 0xc2 | `fingerprintEnabled` (default 1) |
|
|
109
|
+
* | 0xb1 | `passageModeEndTime` | | |
|
|
110
|
+
* | 0xb2 | `isPrivacyMode` (setting-type 9) | | |
|
|
111
|
+
*
|
|
112
|
+
* `a6` is `isOneTouchLock`, an entirely different setting, which is why it never moves with
|
|
113
|
+
* auto-lock's own on/off state — the REAL current-enable-state field is `a1` (our own
|
|
114
|
+
* `parseFf09SettingsResponse` already parses it correctly; it is simply not READ by
|
|
115
|
+
* `sendFf09Autolock`/`dispatchFf09Autolock`, which only pull `a2`/`a4`/`a5`). Not yet
|
|
116
|
+
* wired into any code path — flagged here as a documented opportunity, not a shipped read.
|
|
117
|
+
* See {@link decryptFf09Frame} + {@link parseFf09SettingsResponse}.
|
|
118
|
+
* - **Also live-verified on the T8531 over P2P** (2026-07-18): the same GET/SET frame shape, live-
|
|
119
|
+
* captured off the P2P wire and confirmed byte-identical in structure to the T85D0 capture above
|
|
120
|
+
* (same key derivation, same TLV layout, `A5`'s captured value there was 120s/"2 min" vs. this
|
|
121
|
+
* doc's 90s/"1.5 min" — different device, same field semantics). `dev.lock()?.setAutoLock()` driven
|
|
122
|
+
* end-to-end through the P2P command router against a real T8531, both directions confirmed via the
|
|
123
|
+
* app UI — not just byte-exact against a capture. See `transport/p2p/command-router.ts`'s
|
|
124
|
+
* `sendFf09Autolock`.
|
|
125
|
+
*
|
|
126
|
+
* ## Rain Mode (T8531, 2026-07-18 capture — a SECOND, shorter `SET_SETTINGS` shape)
|
|
127
|
+
*
|
|
128
|
+
* Same `apiCommand`/`commandCode` pair as the auto-lock SET above (6015/52), but a genuinely different,
|
|
129
|
+
* SHORTER plaintext — captured live off the P2P wire toggling Rain Mode off→on: `[A1 time]
|
|
130
|
+
* [A2 adminUserId][A3 1B settingId][A4 1B value]`, nothing past `A4` (no `A5`-`A9` at all). `A3` here
|
|
131
|
+
* is NOT the unconfirmed constant from the full-blob write above — it's a setting-id SELECTOR (`0x07`
|
|
132
|
+
* for Rain Mode), making this a compact single-setting write rather than a read-modify-write of a fixed
|
|
133
|
+
* blob. Because it doesn't touch any field besides the one being set, it's a pure blind write — no GET
|
|
134
|
+
* pass needed first. See {@link FF09_SETTING_ID} + {@link buildFf09SettingToggleFrame}. The `A3`
|
|
135
|
+
* selector addresses at least 14 T8531 settings — the app's own decompiled JS
|
|
136
|
+
* (`smartLockSetParamsDataParse`, 2026-07-18) confirmed the id→setting map; `RAIN_MODE` is the only one
|
|
137
|
+
* independently WIRE-CAPTURED so far, and 6 more (one-touch lock/scramble/wifi/enable-log/privacy/
|
|
138
|
+
* one-touch-rear) are wired but THROW "wire unverified" until each clears that bar — see
|
|
139
|
+
* {@link FF09_SETTING_ID} for the full id list.
|
|
140
|
+
* ✅ LIVE-VERIFIED end-to-end (2026-07-18): `dev.lock()?.setRainMode()` driven through the P2P command
|
|
141
|
+
* router against a real T8531, both directions (off→on and on→off) confirmed via the app UI showing
|
|
142
|
+
* the new state afterward — not just byte-exact against a capture. See
|
|
143
|
+
* `transport/p2p/command-router.ts`'s `sendFf09SettingToggle`.
|
|
144
|
+
*/
|
|
145
|
+
import type { AutoLockSnapshot } from "../core/contracts.js";
|
|
146
|
+
/**
|
|
147
|
+
* `apiCommand` values for the `TRANSFER_PAYLOAD` (1940) envelope `payload`. `ON_OFF_LOCK` (6018)
|
|
148
|
+
* is THE lock/unlock command for both the video lock (T8531) and the garage door (T85D0) — proven
|
|
149
|
+
* end-to-end against a T8531 (unlock/lock/unlock/lock capture) and, 2026-07-16, against the garage
|
|
150
|
+
* door too (2 independent live captures of the real app closing it, both `apiCommand 6018`, matching
|
|
151
|
+
* eufy-sdk's own already-verified open path). Direction is the ff09 frame's `A3` byte, not the
|
|
152
|
+
* apiCommand: both lock and unlock issue `apiCommand 6018`.
|
|
153
|
+
*/
|
|
154
|
+
export declare const LOCK_API_COMMAND: {
|
|
155
|
+
/** Lock / unlock the deadbolt — video lock AND garage door, both directions (see above). */
|
|
156
|
+
readonly ON_OFF_LOCK: 6018;
|
|
157
|
+
/**
|
|
158
|
+
* NOT used by this encoder — kept only as a real, observed constant. `apiCommand 6012` genuinely
|
|
159
|
+
* appears in live traffic (captured from a real but UNIDENTIFIED iOS app instance on the same
|
|
160
|
+
* account, 2026-07-16), but sending it from eufy-sdk never once produced a physical actuation in
|
|
161
|
+
* several live tests. The app's own `ESLCommand` enum (carved from a live `/proc/mem` dump,
|
|
162
|
+
* 2026-07-18) names 6012 `QUERY_STATUS_IN_LOCK` — a read-only status POLL, not an actuator at all,
|
|
163
|
+
* which is exactly why it never once opened/closed anything. The name `OPEN_DOOR` is a misnomer,
|
|
164
|
+
* kept as-is to avoid API churn on a constant that is not usable — do not use it for open, close, or
|
|
165
|
+
* a status read (this encoder has no query-status support; `GET_SETTINGS`/6016 is the only supported
|
|
166
|
+
* read path).
|
|
167
|
+
*/
|
|
168
|
+
readonly OPEN_DOOR: 6012;
|
|
169
|
+
/** Read the current auto-lock/settings TLV — {@link buildFf09QueryFrame}. Verified live 2026-07-16. */
|
|
170
|
+
readonly GET_SETTINGS: 6016;
|
|
171
|
+
/** Write the auto-lock/settings TLV — {@link buildFf09SettingsFrame}. Verified live 2026-07-16. */
|
|
172
|
+
readonly SET_SETTINGS: 6015;
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* `commandCode` values encoded into the `ff09` frame's `cmdEnc` field (`0x4000 | commandCode`).
|
|
176
|
+
* `ON_OFF_LOCK` (35 = 0x23 → cmdEnc 0x4023) is lock/unlock — video lock AND garage door, both
|
|
177
|
+
* directions. Distinct from {@link LOCK_API_COMMAND}: this is the inner frame opcode, that is the
|
|
178
|
+
* outer envelope's `apiCommand`.
|
|
179
|
+
*/
|
|
180
|
+
export declare const LOCK_COMMAND_CODE: {
|
|
181
|
+
/** Lock / unlock the deadbolt (inner frame opcode) — video lock + garage door, both directions. */
|
|
182
|
+
readonly ON_OFF_LOCK: 35;
|
|
183
|
+
/** NOT used by this encoder — see {@link LOCK_API_COMMAND.OPEN_DOOR}; kept as an observed constant only. */
|
|
184
|
+
readonly OPEN_DOOR: 34;
|
|
185
|
+
/** Settings GET inner opcode (53 = 0x35 → cmdEnc 0x4035) — see {@link buildFf09QueryFrame}. */
|
|
186
|
+
readonly GET_SETTINGS: 53;
|
|
187
|
+
/** Settings SET inner opcode (52 = 0x34 → cmdEnc 0x4034) — see {@link buildFf09SettingsFrame}. */
|
|
188
|
+
readonly SET_SETTINGS: 52;
|
|
189
|
+
};
|
|
190
|
+
/** The `TRANSFER_PAYLOAD` inner-command id both envelopes carry (P2P `SET_PAYLOAD.cmd` / MQTT `trans.cmd`). */
|
|
191
|
+
export declare const CMD_TRANSFER_PAYLOAD = 1940;
|
|
192
|
+
export interface Ff09FrameInput {
|
|
193
|
+
/** Actuation direction — the `A3` byte: `true` = engage (byte 0, lock/close), `false` = release (byte 1, unlock/open). */
|
|
194
|
+
engage: boolean;
|
|
195
|
+
/** The lock owner's account id (`member.admin_user_id`, 40 hex chars) — drives the key + `A2`. */
|
|
196
|
+
adminUserId: string;
|
|
197
|
+
/** The acting user's display name (the login email's local-part) — the `A4` field. Omitted when {@link omitUserFields}. */
|
|
198
|
+
username?: string;
|
|
199
|
+
/** The acting member's short id (`member.short_user_id`, hex, e.g. `"0003"`) — the `A5` field. Omitted when {@link omitUserFields}. */
|
|
200
|
+
shortUserId?: string;
|
|
201
|
+
/**
|
|
202
|
+
* Skip the user-attribution TLV fields (A4 username, A5 shortUserId). ✅ The garage-door OPEN command
|
|
203
|
+
* (`apiCommand 6012` / opcode 34) omits them — its plaintext is just `[A1 time, A2 adminUserId, A3 byte]`
|
|
204
|
+
* — verified byte-exact from a live capture. Lock/unlock + garage CLOSE keep them (default false).
|
|
205
|
+
*/
|
|
206
|
+
omitUserFields?: boolean;
|
|
207
|
+
/** The lock's serial number — the AES IV (zero-padded to 16). */
|
|
208
|
+
deviceSn: string;
|
|
209
|
+
/** Override the unix-seconds stamp (testing / byte-exact reproduction). Default = now. */
|
|
210
|
+
unixTime?: number;
|
|
211
|
+
/** Override the key-time nonce OR'd into `unixTime` (testing). Default = a random 0..99. */
|
|
212
|
+
nonce?: number;
|
|
213
|
+
/** Override the envelope `seq_num` (testing). Default = `unixTime`. */
|
|
214
|
+
seqNum?: number;
|
|
215
|
+
}
|
|
216
|
+
export interface Ff09Frame {
|
|
217
|
+
/** The `ff09…` frame, hex — goes verbatim into the envelope `lock_payload` (P2P) or `trans` (MQTT). */
|
|
218
|
+
lockPayload: string;
|
|
219
|
+
/** The cleartext `time` the envelope must carry so the device re-derives the key. */
|
|
220
|
+
time: number;
|
|
221
|
+
/** The envelope `seq_num`. */
|
|
222
|
+
seqNum: number;
|
|
223
|
+
/**
|
|
224
|
+
* The `apiCommand` the envelope must carry for THIS frame (the `TRANSFER_PAYLOAD` `payload.apiCommand`
|
|
225
|
+
* — see {@link LOCK_API_COMMAND}). Returned by the builder so neither transport has to know the
|
|
226
|
+
* frame↔apiCommand pairing: the P2P/MQTT envelope just carries it through. Distinct from the inner
|
|
227
|
+
* frame `commandCode` the builder also picks internally.
|
|
228
|
+
*/
|
|
229
|
+
apiCommand: number;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The `TRANSFER_PAYLOAD` (1940) `payload` object both transports carry for an ff09 frame. The index
|
|
233
|
+
* signature keeps it assignable to the routers' generic `Record<string, unknown>` envelope-body param
|
|
234
|
+
* (every field is already `number | string`).
|
|
235
|
+
*/
|
|
236
|
+
export interface Ff09TransferPayload {
|
|
237
|
+
apiCommand: number;
|
|
238
|
+
lock_payload: string;
|
|
239
|
+
seq_num: number;
|
|
240
|
+
time: number;
|
|
241
|
+
[field: string]: number | string;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Project a built {@link Ff09Frame} into the `TRANSFER_PAYLOAD` (1940) `payload` — the ONE place the
|
|
245
|
+
* frame→envelope field mapping (`lockPayload`→`lock_payload`, `seqNum`→`seq_num`) lives, so neither the
|
|
246
|
+
* P2P router (`set-payload` body) nor the MQTT router (`trans.payload`) re-spells it.
|
|
247
|
+
*/
|
|
248
|
+
export declare function ff09TransferPayload(frame: Ff09Frame): Ff09TransferPayload;
|
|
249
|
+
/**
|
|
250
|
+
* Build an `ff09` command frame. Deterministic given `unixTime`/`nonce`/`seqNum`, so a test can
|
|
251
|
+
* reproduce a captured frame byte-for-byte; otherwise it stamps a fresh `time` per call.
|
|
252
|
+
*
|
|
253
|
+
* Throws on missing identity (empty `adminUserId`/`shortUserId`/`deviceSn`) — an all-empty field would
|
|
254
|
+
* silently produce a frame the device rejects, and both transports send this fire-and-forget (no
|
|
255
|
+
* error comes back), so we fail loud here instead.
|
|
256
|
+
*/
|
|
257
|
+
export declare function buildFf09Frame(input: Ff09FrameInput): Ff09Frame;
|
|
258
|
+
export interface Ff09QueryFrameInput {
|
|
259
|
+
/** The lock owner's account id (`member.admin_user_id`, 40 hex chars) — drives the key + `A2`. */
|
|
260
|
+
adminUserId: string;
|
|
261
|
+
/** The lock's serial number — the AES IV (zero-padded to 16). */
|
|
262
|
+
deviceSn: string;
|
|
263
|
+
/** Override the unix-seconds stamp (testing / byte-exact reproduction). Default = now. */
|
|
264
|
+
unixTime?: number;
|
|
265
|
+
/** Override the key-time nonce OR'd into `unixTime` (testing). Default = a random 0..99. */
|
|
266
|
+
nonce?: number;
|
|
267
|
+
/** Override the envelope `seq_num` (testing). Default = `unixTime`. */
|
|
268
|
+
seqNum?: number;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Build the settings **GET** query frame (`LOCK_COMMAND_CODE.GET_SETTINGS`) — plaintext is just
|
|
272
|
+
* `[A1 time][A2 adminUserId]`, no user-attribution fields (narrower than `omitUserFields` on
|
|
273
|
+
* {@link buildFf09Frame}, which still keeps A3). See the module doc's "Settings GET/SET" section.
|
|
274
|
+
*/
|
|
275
|
+
export declare function buildFf09QueryFrame(input: Ff09QueryFrameInput): Ff09Frame;
|
|
276
|
+
export interface Ff09SettingsFrameInput {
|
|
277
|
+
/** The lock owner's account id (`member.admin_user_id`, 40 hex chars) — drives the key + `A2`. */
|
|
278
|
+
adminUserId: string;
|
|
279
|
+
/** The lock's serial number — the AES IV (zero-padded to 16). */
|
|
280
|
+
deviceSn: string;
|
|
281
|
+
/** `A4` — auto-lock enable (confirmed semantics). */
|
|
282
|
+
autoLockEnabled: boolean;
|
|
283
|
+
/** `A5` (u16 LE) — auto-lock delay in seconds (confirmed semantics; captured value 90 = "1.5 min"). */
|
|
284
|
+
autoLockDelaySeconds: number;
|
|
285
|
+
/**
|
|
286
|
+
* `A7` (u16 LE) — semantics UNCONFIRMED, but the value IS readable back from a GET response
|
|
287
|
+
* (response field `a4`), so callers should pass through a just-read current value rather than a
|
|
288
|
+
* guess. See the module doc's "Settings GET/SET" section.
|
|
289
|
+
*/
|
|
290
|
+
a7: number;
|
|
291
|
+
/** `A8` (u16 LE) — same caveat as {@link a7} (readable back as GET response field `a5`). */
|
|
292
|
+
a8: number;
|
|
293
|
+
/** Override the unix-seconds stamp (testing / byte-exact reproduction). Default = now. */
|
|
294
|
+
unixTime?: number;
|
|
295
|
+
/** Override the key-time nonce OR'd into `unixTime` (testing). Default = a random 0..99. */
|
|
296
|
+
nonce?: number;
|
|
297
|
+
/** Override the envelope `seq_num` (testing). Default = `unixTime`. */
|
|
298
|
+
seqNum?: number;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Build the settings **SET** frame (`LOCK_COMMAND_CODE.SET_SETTINGS`) — plaintext is `[A1 time]
|
|
302
|
+
* [A2 adminUserId][A3 1B][A4 1B enable][A5 2B LE delay][A6 1B][A7 2B LE][A8 2B LE][A9 1B]`. `A3`/`A6`/
|
|
303
|
+
* `A9` are sent as the literal `0x00` observed in the one capture available (semantics unconfirmed —
|
|
304
|
+
* see the module doc). `A7`/`A8` are NOT hardcoded here — the caller (the read-modify-write orchestration
|
|
305
|
+
* in the client) is expected to pass through values just read from a GET response.
|
|
306
|
+
*/
|
|
307
|
+
export declare function buildFf09SettingsFrame(input: Ff09SettingsFrameInput): Ff09Frame;
|
|
308
|
+
/**
|
|
309
|
+
* Setting-id selectors for the COMPACT single-setting `SET_SETTINGS` write ({@link
|
|
310
|
+
* buildFf09SettingToggleFrame}) — a SHORTER TLV than the full-blob write ({@link buildFf09SettingsFrame},
|
|
311
|
+
* which always sends `A3=0x00` plus all of `A4`-`A9` together for auto-lock). Captured live on a T8531
|
|
312
|
+
* (2026-07-18): toggling Rain Mode off→on produced just `[A1 time][A2 adminUserId][A3=0x07][A4 0/1]` —
|
|
313
|
+
* nothing past `A4`, and `A3` here is a setting-id SELECTOR rather than an unconfirmed constant. The
|
|
314
|
+
* app's own decompiled JS (2026-07-18) mapped the full selector enum (≥14 settings); `RAIN_MODE` is the
|
|
315
|
+
* only one independently wire-captured, the other 6 ids below ship throwing "wire unverified" pending
|
|
316
|
+
* their own capture. See the module doc's "Rain Mode" section.
|
|
317
|
+
*
|
|
318
|
+
* NOT consulted by {@link buildFf09SettingToggleFrame} or anything else on the production path — the
|
|
319
|
+
* wire byte actually comes from the `Command`'s opaque `settingId` field, sourced from `model/`'s OWN
|
|
320
|
+
* copy (`lock.ts`'s `LOCK_SETTING_ID.RAIN_MODE`, per the capability↔transport decorrelation rule this
|
|
321
|
+
* module's doc already covers). This constant is layer-local wire vocab for THIS file's own callers/
|
|
322
|
+
* spec only — it documents the value but carries no production reference, so it can't structurally
|
|
323
|
+
* catch the two copies drifting apart; see `lock.ts`'s doc for that caveat.
|
|
324
|
+
*/
|
|
325
|
+
export declare const FF09_SETTING_ID: {
|
|
326
|
+
/** One-touch lock toggle. Structurally confirmed via the app's own JS (2026-07-18) — not independently wire-captured. */
|
|
327
|
+
readonly ONE_TOUCH_LOCK: 1;
|
|
328
|
+
/** Scramble-passcode toggle. Structurally confirmed via the app's own JS (2026-07-18) — not independently wire-captured. */
|
|
329
|
+
readonly SCRAMBLE_PASSCODE: 3;
|
|
330
|
+
/** Wifi-status toggle. Structurally confirmed via the app's own JS (2026-07-18) — not independently wire-captured. */
|
|
331
|
+
readonly WIFI_STATUS: 5;
|
|
332
|
+
/** Event-log-enable toggle. Structurally confirmed via the app's own JS (2026-07-18) — not independently wire-captured. */
|
|
333
|
+
readonly ENABLE_LOG: 6;
|
|
334
|
+
/** Rain Mode toggle on the T8531 video lock. `A4` = 0 off / 1 on. Verified live 2026-07-18. */
|
|
335
|
+
readonly RAIN_MODE: 7;
|
|
336
|
+
/** Privacy-mode toggle. Structurally confirmed via the app's own JS (2026-07-18) — not independently wire-captured. */
|
|
337
|
+
readonly PRIVACY_MODE: 9;
|
|
338
|
+
/** One-touch rear-lock toggle. Structurally confirmed via the app's own JS (2026-07-18) — not independently wire-captured. */
|
|
339
|
+
readonly ONE_TOUCH_REAR_LOCK: 11;
|
|
340
|
+
};
|
|
341
|
+
export interface Ff09SettingToggleFrameInput {
|
|
342
|
+
/** The lock owner's account id (`member.admin_user_id`, 40 hex chars) — drives the key + `A2`. */
|
|
343
|
+
adminUserId: string;
|
|
344
|
+
/** The lock's serial number — the AES IV (zero-padded to 16). */
|
|
345
|
+
deviceSn: string;
|
|
346
|
+
/** Which setting this write targets — the `A3` field. See {@link FF09_SETTING_ID}. */
|
|
347
|
+
settingId: number;
|
|
348
|
+
/** The new value — the `A4` field (0/1). */
|
|
349
|
+
value: boolean;
|
|
350
|
+
/** Override the unix-seconds stamp (testing / byte-exact reproduction). Default = now. */
|
|
351
|
+
unixTime?: number;
|
|
352
|
+
/** Override the key-time nonce OR'd into `unixTime` (testing). Default = a random 0..99. */
|
|
353
|
+
nonce?: number;
|
|
354
|
+
/** Override the envelope `seq_num` (testing). Default = `unixTime`. */
|
|
355
|
+
seqNum?: number;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Build the settings **SET** frame using the COMPACT single-setting shape (`LOCK_COMMAND_CODE.SET_SETTINGS`,
|
|
359
|
+
* same opcode as {@link buildFf09SettingsFrame}) — plaintext is just `[A1 time][A2 adminUserId]
|
|
360
|
+
* [A3 1B settingId][A4 1B value]`, no `A5`-`A9` at all. Unlike the full-blob write, this is a pure blind
|
|
361
|
+
* write — no GET-then-preserve is needed since it doesn't touch any field the device isn't being told to
|
|
362
|
+
* change. See the module doc's "Rain Mode" section + {@link FF09_SETTING_ID}.
|
|
363
|
+
*/
|
|
364
|
+
export declare function buildFf09SettingToggleFrame(input: Ff09SettingToggleFrameInput): Ff09Frame;
|
|
365
|
+
/**
|
|
366
|
+
* Decrypt any `ff09` response frame (e.g. the GET-settings reply) back to its plaintext TLV bytes.
|
|
367
|
+
* Same key/iv derivation as the encoder (`adminUserId.slice(-12) ‖ u32be(keyTime)` / `deviceSn` padded
|
|
368
|
+
* 16), but normal PKCS#7 unpadding (`createDecipheriv` with auto-padding ON) — unlike the encoder,
|
|
369
|
+
* which zero-pads its OWN plaintext before letting AES add a genuine PKCS#7 block on top, a device
|
|
370
|
+
* response is a plain PKCS#7-padded ciphertext with no extra zero-padding layer.
|
|
371
|
+
*
|
|
372
|
+
* Validates the `ff09` magic and the trailing XOR checksum (the frame's own self-check), but does NOT
|
|
373
|
+
* validate `cmdEnc` beyond that — a response's `cmdEnc` carries an extra flag bit vs. a request's
|
|
374
|
+
* (`0x48xx` not `0x40xx`) and this decoder is generic across whatever inner opcode it's carrying.
|
|
375
|
+
*
|
|
376
|
+
* `keyTime` is the response envelope's `time` field parsed as the hex string it actually is (NOT
|
|
377
|
+
* decimal, unlike the `seq_num`/other envelope fields) — see the module doc.
|
|
378
|
+
*/
|
|
379
|
+
export declare function decryptFf09Frame(input: {
|
|
380
|
+
lockPayload: string;
|
|
381
|
+
keyTime: number;
|
|
382
|
+
adminUserId: string;
|
|
383
|
+
deviceSn: string;
|
|
384
|
+
}): Buffer;
|
|
385
|
+
/** A decoded settings-response TLV: the leading status byte + the `a1..ad` fields, keyed by separator byte. */
|
|
386
|
+
export interface Ff09SettingsResponse {
|
|
387
|
+
/** The leading status byte (byte 0 of the decrypted plaintext, before the TLV fields). */
|
|
388
|
+
status: number;
|
|
389
|
+
/** `sep` (e.g. `0xa2`) → field bytes. The response's own tag numbering — does NOT line up positionally
|
|
390
|
+
* with the GET/SET request TLVs (see the module doc). */
|
|
391
|
+
fields: Map<number, Buffer>;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Parse a decrypted settings-response plaintext ({@link decryptFf09Frame}'s output) into its leading
|
|
395
|
+
* status byte + `sep|len|bytes` TLV fields. Stops at a `0x00` separator (real tags start at `0xa1`;
|
|
396
|
+
* a `0x00` sep only ever shows up as trailing zero-padding, never a real field) so it doesn't choke on
|
|
397
|
+
* padding past the last real field.
|
|
398
|
+
*/
|
|
399
|
+
export declare function parseFf09SettingsResponse(plain: Buffer): Ff09SettingsResponse;
|
|
400
|
+
/** Read a little-endian u16 out of a TLV field buffer (throws on a missing/short field — a caller-side bug, not a wire ambiguity). */
|
|
401
|
+
export declare function readFf09U16LE(field: Buffer | undefined, name: string): number;
|
|
402
|
+
/** Read a single byte out of a TLV field buffer (throws on a missing field — same convention as {@link readFf09U16LE}). */
|
|
403
|
+
export declare function readFf09U8(field: Buffer | undefined, name: string): number;
|
|
404
|
+
/**
|
|
405
|
+
* Read a 2-byte `[hour, minute]` time pair out of a TLV field buffer — the app encodes schedule times
|
|
406
|
+
* as `hour.toString(16)‖minute.toString(16)` (two raw byte VALUES, not a packed LE u16 — see the
|
|
407
|
+
* module doc's "Settings GET/SET" section). Throws on a missing/short field, same convention as
|
|
408
|
+
* {@link readFf09U16LE}.
|
|
409
|
+
*/
|
|
410
|
+
export declare function readFf09HourMinute(field: Buffer | undefined, name: string): [number, number];
|
|
411
|
+
/**
|
|
412
|
+
* Decode a parsed `GET_SETTINGS` response into the auto-lock snapshot both transports return from
|
|
413
|
+
* `getAutoLockState` — the response tag map's `a1`=enabled, `a2`=delaySeconds, `a3`=isSchedule,
|
|
414
|
+
* `a4`/`a5`=schedule start/end (`[hour,minute]`). Shared here (rather than copied into each router's
|
|
415
|
+
* `getAutoLockState`) so the P2P and MQTT read paths can't drift on the field mapping.
|
|
416
|
+
*/
|
|
417
|
+
export declare function decodeFf09AutoLockSnapshot(parsed: Ff09SettingsResponse): AutoLockSnapshot;
|
|
418
|
+
/**
|
|
419
|
+
* Coerce a settings-response `time` field back to the numeric keyTime, for matching the reply against
|
|
420
|
+
* the GET's own keyTime. Shared by both transports' autolock GET-reply matchers. A string is parsed
|
|
421
|
+
* base-16 (the reply `time` is a hex string on the working, test-backed path — `ff09-mqtt-settings-
|
|
422
|
+
* dispatch.spec` drives the full GET→decrypt→SET flow with a `time.toString(16)` reply); a JSON number
|
|
423
|
+
* is taken as the value directly (unambiguous). A *decimal string* is deliberately NOT handled — it's
|
|
424
|
+
* indistinguishable from hex and never observed on the wire, so parsing an all-digit decimal as hex
|
|
425
|
+
* would silently mis-match. Returns the keyTime, or `undefined` if it doesn't parse to a finite number.
|
|
426
|
+
*/
|
|
427
|
+
export declare function ff09ReplyKeyTime(rawTime: string | number): number | undefined;
|
|
428
|
+
/**
|
|
429
|
+
* The read-modify-write core of an auto-lock settings write, shared by both transports' autolock
|
|
430
|
+
* dispatchers ({@link buildFf09QueryFrame} GET → this → send): decrypt the GET-settings reply, pull the
|
|
431
|
+
* current delay + `A7`/`A8` passthrough values off it, and build the SET frame that changes only
|
|
432
|
+
* enable/delay while preserving everything else. The transport owns getting the reply (its own GET wire
|
|
433
|
+
* + reply match) and wrapping/sending the returned frame; the decrypt→read→rebuild sequence lives here
|
|
434
|
+
* once so the two routers can't drift (e.g. the pending `a2`/`a4`/`a5`→`a1` enable-readback fix the
|
|
435
|
+
* module doc flags is then a one-place change).
|
|
436
|
+
*/
|
|
437
|
+
export declare function buildFf09AutolockSetFrame(input: {
|
|
438
|
+
lockPayload: string;
|
|
439
|
+
keyTime: number;
|
|
440
|
+
adminUserId: string;
|
|
441
|
+
deviceSn: string;
|
|
442
|
+
enabled: boolean;
|
|
443
|
+
delaySeconds?: number;
|
|
444
|
+
}): Ff09Frame;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared `ffmpeg` spawn helper — the ONE place that shells out to ffmpeg. It decides ffmpeg's own
|
|
3
|
+
* verbosity, prepends the common flags, and routes stderr into the SDK's {@link Logger}, so no caller
|
|
4
|
+
* touches `spawn("ffmpeg", …)` directly. Used by the media paths that mux via ffmpeg (`p2p/media.ts`
|
|
5
|
+
* snapshot/record). Not byte-on-a-wire, so it sits at the transport root
|
|
6
|
+
* rather than in a wire subfolder.
|
|
7
|
+
*
|
|
8
|
+
* Two independent dials:
|
|
9
|
+
* - **ffmpeg verbosity** — how chatty ffmpeg itself is, via `-loglevel`, from the host's
|
|
10
|
+
* `new EufyMega({ ffmpegLogLevel })` config (default `"error"`).
|
|
11
|
+
* - **where it lands** — the host {@link Logger} and its own min-level gate what's actually shown.
|
|
12
|
+
*
|
|
13
|
+
* So `new EufyMega({ ffmpegLogLevel: "trace", logger: new ConsoleLogger() })` surfaces ffmpeg's full
|
|
14
|
+
* trace at `[ffmpeg]` debug lines; the default level + no logger stays silent.
|
|
15
|
+
*
|
|
16
|
+
* WHICH binary runs is a third, orthogonal dial: `new EufyMega({ ffmpegPath })`. See
|
|
17
|
+
* {@link ffmpegExecutable} — the SDK never edits the process `PATH`, so an explicit path is what names
|
|
18
|
+
* a binary that is not on it.
|
|
19
|
+
*
|
|
20
|
+
* @module transport/ffmpeg
|
|
21
|
+
*/
|
|
22
|
+
import { type ChildProcess, type StdioOptions } from "node:child_process";
|
|
23
|
+
import { type Logger } from "../core/logger.js";
|
|
24
|
+
/**
|
|
25
|
+
* ffmpeg's `-loglevel` values, quiet → loud. `trace` is the firehose.
|
|
26
|
+
*
|
|
27
|
+
* Exported as the set in data form; not published — `FfmpegLevel` states the same members.
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export declare const FFMPEG_LEVELS: readonly ["quiet", "panic", "fatal", "error", "warning", "info", "verbose", "debug", "trace"];
|
|
31
|
+
/** A valid ffmpeg `-loglevel`. Set via `new EufyMega({ ffmpegLogLevel })`. */
|
|
32
|
+
export type FfmpegLevel = (typeof FFMPEG_LEVELS)[number];
|
|
33
|
+
/**
|
|
34
|
+
* Resolve ffmpeg's own verbosity from the SDK config a host passes (`new EufyMega({ ffmpegLogLevel })`),
|
|
35
|
+
* defaulting to `"error"` (quiet); an unset/invalid value yields the default. This is ffmpeg's
|
|
36
|
+
* `-loglevel`, NOT the SDK's {@link LogLevel} — the two are orthogonal (ffmpeg decides what to emit,
|
|
37
|
+
* the Logger decides what's shown).
|
|
38
|
+
*/
|
|
39
|
+
export declare function ffmpegLogLevel(level?: FfmpegLevel): FfmpegLevel;
|
|
40
|
+
/**
|
|
41
|
+
* Resolve WHICH ffmpeg to run from the SDK config a host passes (`new EufyMega({ ffmpegPath })`),
|
|
42
|
+
* defaulting to the bare name `"ffmpeg"` so it is looked up on `PATH`.
|
|
43
|
+
*
|
|
44
|
+
* An environment where no `ffmpeg` is on `PATH` is ordinary, and the SDK never mutates
|
|
45
|
+
* `process.env.PATH` process-wide to reach one — this option is how such a binary is named.
|
|
46
|
+
*
|
|
47
|
+
* A **blank** value counts as absent: an unset host config commonly arrives as `""` or as whitespace
|
|
48
|
+
* from a config file, and neither can name a binary, so spawning it would fail as an `ENOENT` on the
|
|
49
|
+
* empty string — the misleading message this option exists to remove. Any non-blank value is passed
|
|
50
|
+
* through EXACTLY as given, never trimmed: a leading or trailing space is legal in a POSIX path, and
|
|
51
|
+
* rewriting one would make a real file unreachable.
|
|
52
|
+
*
|
|
53
|
+
* The value is NOT probed here. Spawn failure surfaces to the caller as the media path's own "not
|
|
54
|
+
* runnable" rejection, which is the same signal a missing `PATH` entry gives, so there is nothing for
|
|
55
|
+
* an extra `stat` to add. {@link ffmpegAvailable} is the probe.
|
|
56
|
+
*/
|
|
57
|
+
export declare function ffmpegExecutable(path?: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* Spawn ffmpeg. Prepends `-hide_banner` + the env-driven `-loglevel` (see {@link ffmpegLogLevel}) to
|
|
60
|
+
* `args`, then forwards the child's stderr to `logger.debug` under an `[ffmpeg]` prefix — the logger's
|
|
61
|
+
* min-level gates visibility, so a `noopLogger` (the default) swallows it. Callers pass only their
|
|
62
|
+
* ffmpeg-specific args and read stdin/stdout off the returned child.
|
|
63
|
+
*
|
|
64
|
+
* `stdio` defaults to all-pipe; pass e.g. `["pipe", "ignore", "pipe"]` to drop stdout (stderr must
|
|
65
|
+
* stay piped for forwarding to work). `level` overrides the resolved `-loglevel` (see
|
|
66
|
+
* {@link ffmpegLogLevel} for precedence); `executable` picks the binary (see
|
|
67
|
+
* {@link ffmpegExecutable}).
|
|
68
|
+
*/
|
|
69
|
+
export interface FfmpegSpawnOptions {
|
|
70
|
+
logger?: Logger;
|
|
71
|
+
stdio?: StdioOptions;
|
|
72
|
+
level?: FfmpegLevel;
|
|
73
|
+
/** The ffmpeg binary to run. Default: the bare name, looked up on `PATH`. */
|
|
74
|
+
executable?: string;
|
|
75
|
+
}
|
|
76
|
+
export declare function spawnFfmpeg(args: string[], opts?: FfmpegSpawnOptions): ChildProcess;
|
|
77
|
+
/**
|
|
78
|
+
* Whether the resolved ffmpeg is runnable, by running `-version` on it. Resolves the executable exactly
|
|
79
|
+
* as the media paths do, so the answer is about the SAME binary they will launch.
|
|
80
|
+
*/
|
|
81
|
+
export declare function ffmpegAvailable(executable?: string): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Whether `ffprobe` is on `PATH`. The SDK never spawns it, so there is no executable to resolve and
|
|
84
|
+
* none is taken.
|
|
85
|
+
*/
|
|
86
|
+
export declare function ffprobeAvailable(): boolean;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const V1_PREFIX = "eufysecurity";
|
|
2
|
+
/** Derive the numeric id-suffix from the station `p2p_did` — a digit-mixing seed used by the key derivation. */
|
|
3
|
+
export declare function getIdSuffix(p2pDid: string): number;
|
|
4
|
+
/** Derive the image "base code" from the camera serial + `p2p_did` — first input to {@link getImageKey}. */
|
|
5
|
+
export declare function getImageBaseCode(serialNumber: string, p2pDid: string): string;
|
|
6
|
+
/** Derive the per-image seed from the `p2p_did` + the image's own code — second input to {@link getImageKey}. */
|
|
7
|
+
export declare function getImageSeed(p2pDid: string, code: string): string;
|
|
8
|
+
/** Derive the AES-128-ECB key for a v1 thumbnail: SHA-256 over the base code + seed, rotated by a hash byte. */
|
|
9
|
+
export declare function getImageKey(serialNumber: string, p2pDid: string, code: string): string;
|
|
10
|
+
/** True if the blob is a legacy v1 `eufysecurity:` image (NOT the v2 variant). */
|
|
11
|
+
export declare function isV1Image(data: Buffer): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Decode a v1 `eufysecurity:` blob to a plain JPEG buffer using the station's
|
|
14
|
+
* `p2p_did`. Returns null if the blob isn't v1. Mirrors the legacy decode: parse
|
|
15
|
+
* SERIAL + CODE, derive the key, AES-128-ECB-decrypt the first 256 bytes, splice
|
|
16
|
+
* the decrypted head back in front of the plaintext tail.
|
|
17
|
+
*/
|
|
18
|
+
export declare function decodeImageV1(data: Buffer, p2pDid: string): Buffer | null;
|
|
19
|
+
/** Decode a recognized v1 (device-key) or v2 (keyless) wrapper; leave other media unchanged. */
|
|
20
|
+
export declare function normalizePushImage(data: Buffer, p2pDid?: string): Buffer;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-channel auto-contrast, a byte-exact port of PIL `ImageOps.autocontrast` (cutoff 0.5%). The lost
|
|
3
|
+
* quant tables leave the reconstruction low-contrast ("foggy"); stretching each channel's clipped range
|
|
4
|
+
* to full scale restores a natural-looking image. Mutates `data` (RGBA) in place.
|
|
5
|
+
*
|
|
6
|
+
* Parity notes vs PIL: the cutoff count is `n * cutoff // 100` (integer floor); it is trimmed off each
|
|
7
|
+
* end by zeroing whole histogram bins until the count is spent; the range is then the first/last
|
|
8
|
+
* non-empty bins; and the LUT truncates toward zero (`int()`), NOT rounds — rounding would shift pixels.
|
|
9
|
+
*/
|
|
10
|
+
export declare function autoContrast(data: Uint8Array, width: number, height: number, cutoff?: number): void;
|
|
11
|
+
/** True if the blob is a v2 `v2_eufysecurity:` push thumbnail. */
|
|
12
|
+
export declare function isV2Image(data: Buffer): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Decode a v2 blob to a plain JPEG buffer by reconstructing its header, or null if it isn't v2 or the
|
|
15
|
+
* plaintext scan can't be located. The search first chooses subsampling and coarse geometry, derives
|
|
16
|
+
* the fixed MCU count, refines width by row shear, and pins the exact fill height before applying
|
|
17
|
+
* auto-contrast and re-encoding. See the module doc for the keyless-splice rationale.
|
|
18
|
+
*/
|
|
19
|
+
export declare function decodeImageV2(data: Buffer): Buffer | null;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export * from "./mega-client.js";
|
|
2
|
+
export { randomPhoneModel, randomUserAgent } from "./phone-model.js";
|
|
3
|
+
export * from "./decodeImageV1.js";
|
|
4
|
+
export * from "./decodeImageV2.js";
|
|
5
|
+
export { listLightEffects, listAiSceneRecommendations, type LightEffectSummary } from "./light-catalog.js";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Light-effect **catalogue** over the vendor's `light` HTTP service — `/app/light/discover/list`,
|
|
3
|
+
* `/app/light/lighteffect/batchget` and `/app/light/aigc/recommend/list`, with their response parsing.
|
|
4
|
+
* `light` here is the service path the vendor publishes, which is why this sits with the rest of the
|
|
5
|
+
* HTTP surface.
|
|
6
|
+
*
|
|
7
|
+
* This module fetches a catalogue definition; `transport/dp-preset.ts` turns one into wire fields, and
|
|
8
|
+
* sits at the transport root because both this and the MQTT router serialize through it.
|
|
9
|
+
*
|
|
10
|
+
* The catalogue is keyed by numeric id throughout and carries no model vocabulary.
|
|
11
|
+
*/
|
|
12
|
+
import type { MegaHttpClient } from "./mega-client.js";
|
|
13
|
+
import type { DpPresetSpec } from "../dp-preset.js";
|
|
14
|
+
/**
|
|
15
|
+
* One entry in the light-effect gallery — the browsable catalogue behind {@link listLightEffects}.
|
|
16
|
+
* `buildable` says whether the entry's definition can be turned into wire fields at all: false when it
|
|
17
|
+
* carries no layers, or a layer shape this SDK cannot encode.
|
|
18
|
+
*/
|
|
19
|
+
export interface LightEffectSummary {
|
|
20
|
+
/** The catalogue id that identifies this effect on the wire. */
|
|
21
|
+
lightId: number;
|
|
22
|
+
/** Display name from the catalogue (e.g. "Presidents Day"), when present. */
|
|
23
|
+
name?: string;
|
|
24
|
+
/** Preview swatch — `"RRGGBB|RRGGBB…"` — when the catalogue entry carries one. */
|
|
25
|
+
colors?: string;
|
|
26
|
+
/** Whether this entry can be serialized to wire fields. */
|
|
27
|
+
buildable: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Browse the light-effect gallery — the catalogue of `lightId`s that `setEffect` accepts. Unions every
|
|
31
|
+
* scene/light id found anywhere in the `/app/light/discover/list` carousel (walked recursively) with a
|
|
32
|
+
* scan over the default `LIGHT_EFFECT_ID_WINDOW` (the dense id band holding the app's category tabs) plus any
|
|
33
|
+
* `ids` given, then resolves them via `/app/light/lighteffect/batchget` (which returns an entry only
|
|
34
|
+
* for ids that exist). Pass `idRange` to widen/narrow the scan or `ids` to fetch specific ones.
|
|
35
|
+
*
|
|
36
|
+
* `batchget` is issued in chunks of `BATCHGET_CHUNK` (100) ids — the scanned set runs to the hundreds,
|
|
37
|
+
* so one request does not swallow it whole.
|
|
38
|
+
*/
|
|
39
|
+
export declare function listLightEffects(mega: MegaHttpClient, opts?: {
|
|
40
|
+
idRange?: [number, number];
|
|
41
|
+
ids?: number[];
|
|
42
|
+
}): Promise<LightEffectSummary[]>;
|
|
43
|
+
/**
|
|
44
|
+
* List the **AI-generated ambient scenes** ("aigc") — e.g. "Enchanting Starry Night", "Moonlit
|
|
45
|
+
* Serenity". A DIFFERENT resource from {@link listLightEffects}'s id-addressable catalogue:
|
|
46
|
+
* `/app/light/aigc/recommend/list` returns scene **keywords only, with NO `lightId`**, so they can't be
|
|
47
|
+
* driven through `setEffect` — the app turns a chosen keyword into an applied effect via a server-side
|
|
48
|
+
* generate step the SDK hasn't reversed yet. Surfaced for visibility. Needs a `region` (400s
|
|
49
|
+
* without one) and, for some accounts, a device `sn`; `region` defaults to `"eu"` (the endpoint
|
|
50
|
+
* returned an identical list across every shard tried).
|
|
51
|
+
*/
|
|
52
|
+
export declare function listAiSceneRecommendations(mega: MegaHttpClient, opts?: {
|
|
53
|
+
region?: string;
|
|
54
|
+
sn?: string;
|
|
55
|
+
}): Promise<string[]>;
|
|
56
|
+
/**
|
|
57
|
+
* Fetch a gallery effect by exact catalog `lightId` and parse it into the {@link DpPresetSpec} the
|
|
58
|
+
* MQTT router serializes — the "Auto inside setEffect" resolution. Matches the EXACT id (never
|
|
59
|
+
* substitutes a neighbour — the DP write is fire-and-forget). Only effects with directly-serializable
|
|
60
|
+
* `params.layer` data are supported; flat/grouped entries throw a clear error rather than emit a guess.
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveLightEffect(mega: MegaHttpClient, lightId: number): Promise<DpPresetSpec>;
|