@mega-yfue/eufy-sdk 0.5.0-beta.88 → 0.5.0-beta.89
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 +4 -2
- package/dist/core/contracts.d.ts +13 -4
- package/dist/index.js +318 -36
- package/dist/index.js.map +4 -4
- package/dist/model/capabilities/lock.d.ts +23 -11
- package/dist/model/device-family.d.ts +12 -0
- package/dist/transport/http/mega-client.d.ts +9 -0
- package/dist/transport/p2p/codec.d.ts +16 -0
- package/dist/transport/p2p/command-router.d.ts +73 -0
- package/dist/transport/p2p/keyed-payload.d.ts +112 -0
- package/dist/transport/p2p/p2p-session.d.ts +5 -0
- package/package.json +1 -1
|
@@ -435,8 +435,10 @@ export declare class EufyMega extends EventEmitter {
|
|
|
435
435
|
* Choose the transport stack for one command — the routing half of {@link commandSinkFor}.
|
|
436
436
|
*
|
|
437
437
|
* The `ff09-*` kinds share ONE frame that rides either transport, so each stack is asked whether it
|
|
438
|
-
* drives this device (`claimsDevice`) rather than inferring from the kind
|
|
439
|
-
*
|
|
438
|
+
* drives this device (`claimsDevice`) rather than inferring from the kind; `keyed-payload-actuate` (the
|
|
439
|
+
* classic Wi-Fi lock) is routed the same way, a channel-less actuation claimed by the stack that drives
|
|
440
|
+
* the lock. `registry.require` throws on an unloaded serial, so a routing decision never silently falls
|
|
441
|
+
* through to the wrong transport.
|
|
440
442
|
* The `eufy_life` DP writes (smart lights) are secure-MQTT-only. `aiot-dp` routes to either the
|
|
441
443
|
* Anker AIoT MQTT stack or the legacy Tuya REST router depending on the device's category
|
|
442
444
|
* (`eufy_home_tuya` → Tuya, everything else → MQTT). The capability layer emits a single `aiot-dp`
|
package/dist/core/contracts.d.ts
CHANGED
|
@@ -264,10 +264,11 @@ export declare class StateConvergenceError extends Error {
|
|
|
264
264
|
*/
|
|
265
265
|
export type ScalarForm = "auto" | "int-string" | "direct-binary";
|
|
266
266
|
/**
|
|
267
|
-
* The identity fields
|
|
268
|
-
* plus the frame's own direction bit and user-attribution fields
|
|
269
|
-
*
|
|
270
|
-
*
|
|
267
|
+
* The identity fields an actuation frame is built from — for `ff09`, the AES key/IV inputs
|
|
268
|
+
* (`adminUserId`/`deviceSn`) plus the frame's own direction bit and user-attribution fields
|
|
269
|
+
* (`A3`/`A4`/`A5`); for the keyed-payload envelope, the same five fields, sealed into its inner JSON.
|
|
270
|
+
* Shared by both actuate intents; the `ff09` settings intents carry a narrower subset (the GET/SET frames
|
|
271
|
+
* omit the user-attribution fields).
|
|
271
272
|
*/
|
|
272
273
|
export interface Ff09Identity {
|
|
273
274
|
/** Actuation direction: `true` = engage (lock / close), `false` = release (unlock / open) — the frame's `A3` byte. */
|
|
@@ -353,6 +354,14 @@ export type Command = {
|
|
|
353
354
|
channel: number;
|
|
354
355
|
} | ({
|
|
355
356
|
kind: "ff09-actuate";
|
|
357
|
+
} & Ff09Identity)
|
|
358
|
+
/**
|
|
359
|
+
* `keyed-payload-actuate` — a `SET_PAYLOAD` envelope whose inner JSON is sealed with a fresh AES key
|
|
360
|
+
* that the envelope itself carries, wrapped for the device's own public key. P2P-only; the router
|
|
361
|
+
* resolves the device key and the member display name from the device record.
|
|
362
|
+
*/
|
|
363
|
+
| ({
|
|
364
|
+
kind: "keyed-payload-actuate";
|
|
356
365
|
} & Ff09Identity) | {
|
|
357
366
|
kind: "ff09-autolock";
|
|
358
367
|
adminUserId: string;
|
package/dist/index.js
CHANGED
|
@@ -2160,6 +2160,32 @@ ${this.auth_?.authToken ?? ""}`;
|
|
|
2160
2160
|
const shard = this.region.split("-")[0];
|
|
2161
2161
|
return shard === "us" ? "security-app.eufylife.com" : `security-app-${shard}.eufylife.com`;
|
|
2162
2162
|
}
|
|
2163
|
+
/**
|
|
2164
|
+
* The device's long-lived P-256 public key, as the cloud publishes it for key-wrapped commands
|
|
2165
|
+
* (`type=2`): 64 bytes hex with no `04` prefix. A plain authed GET on the security-app host — the
|
|
2166
|
+
* one read here that is neither signed nor body-encrypted, matching how the app fetches it.
|
|
2167
|
+
* Cached per serial for the client's lifetime; the key does not rotate in normal operation.
|
|
2168
|
+
*/
|
|
2169
|
+
async getDevicePublicKey(deviceSn) {
|
|
2170
|
+
const cached2 = this.devicePublicKeys.get(deviceSn);
|
|
2171
|
+
if (cached2)
|
|
2172
|
+
return cached2;
|
|
2173
|
+
const url = `https://${this.securityAppHost()}/v1/app/public_key/query?device_sn=${encodeURIComponent(deviceSn)}&type=2`;
|
|
2174
|
+
const res = await fetch(url, {
|
|
2175
|
+
method: "GET",
|
|
2176
|
+
headers: { ...this.baseHeaders(), ...this.authTokenHeaders() },
|
|
2177
|
+
signal: AbortSignal.timeout(2e4)
|
|
2178
|
+
});
|
|
2179
|
+
const env = parseMaybeJson(await res.text());
|
|
2180
|
+
const key = env?.data?.public_key;
|
|
2181
|
+
if (res.status !== 200 || env?.code !== 0 || typeof key !== "string" || !key.length) {
|
|
2182
|
+
throw new MegaApiError(`public_key/query failed (${res.status}/${env?.code}): ${env?.msg ?? "no public key"}`, env?.code, res.status);
|
|
2183
|
+
}
|
|
2184
|
+
this.devicePublicKeys.set(deviceSn, key);
|
|
2185
|
+
return key;
|
|
2186
|
+
}
|
|
2187
|
+
/** Per-serial cache for {@link getDevicePublicKey}. */
|
|
2188
|
+
devicePublicKeys = /* @__PURE__ */ new Map();
|
|
2163
2189
|
/**
|
|
2164
2190
|
* Signed+encrypted POST to the security-app data host (face recognition, etc.).
|
|
2165
2191
|
* Despite the different host, these endpoints use the SAME algo_ecdh pipeline as
|
|
@@ -3661,6 +3687,7 @@ var isDoorbellT8223 = (ctx) => ctx.model === "T8223";
|
|
|
3661
3687
|
var isAiotVacuum = (ctx) => ctx.category !== "eufy_home_tuya";
|
|
3662
3688
|
var isTuyaVacuum = (ctx) => ctx.category === "eufy_home_tuya";
|
|
3663
3689
|
var isVideoSmartLock = (ctx) => ctx.deviceType === DeviceType.LOCK_8530;
|
|
3690
|
+
var isClassicWifiLock = (ctx) => ctx.deviceType === DeviceType.LOCK_WIFI && typeof ctx.serial === "string" && ctx.serial.startsWith("T8520") && ctx.serial.charAt(6) === "2";
|
|
3664
3691
|
|
|
3665
3692
|
// dist/model/capabilities/rtsp.js
|
|
3666
3693
|
var RTSP_PARAM = {
|
|
@@ -7535,7 +7562,7 @@ function actuate(engage, ctx, sink) {
|
|
|
7535
7562
|
});
|
|
7536
7563
|
}
|
|
7537
7564
|
return sink.dispatch({
|
|
7538
|
-
kind: "ff09-actuate",
|
|
7565
|
+
kind: isClassicWifiLock(ctx) ? "keyed-payload-actuate" : "ff09-actuate",
|
|
7539
7566
|
engage,
|
|
7540
7567
|
adminUserId: ctx.adminUserId,
|
|
7541
7568
|
username: ctx.accountName ?? "",
|
|
@@ -7580,6 +7607,8 @@ function nativeReport(signal) {
|
|
|
7580
7607
|
const report = signal.json.payload;
|
|
7581
7608
|
return report?.code === 0 ? report : void 0;
|
|
7582
7609
|
}
|
|
7610
|
+
var speaksFf09Settings = (ctx) => !isClassicWifiLock(ctx);
|
|
7611
|
+
var rainModeAvailable = (ctx) => overP2p(ctx) && speaksFf09Settings(ctx);
|
|
7583
7612
|
var LOCK_MEMBERS = {
|
|
7584
7613
|
/**
|
|
7585
7614
|
* The lock's own state, verified on param 6000: "4" = locked, "3" = unlocked.
|
|
@@ -7610,8 +7639,8 @@ var LOCK_MEMBERS = {
|
|
|
7610
7639
|
description: "Lock battery level 0-100 (verified: param 1101, alias 6001)."
|
|
7611
7640
|
},
|
|
7612
7641
|
/**
|
|
7613
|
-
* Link quality in dBm as the lock measures it, on the shared param 1141.
|
|
7614
|
-
* are fire-and-forget, so a weak link is silent rather than an error.
|
|
7642
|
+
* Link quality in dBm as the lock measures it, on the shared param 1141. On the `ff09` wire the
|
|
7643
|
+
* actuations are fire-and-forget, so a weak link is silent rather than an error.
|
|
7615
7644
|
*/
|
|
7616
7645
|
rssi: {
|
|
7617
7646
|
param: 1141,
|
|
@@ -7621,9 +7650,15 @@ var LOCK_MEMBERS = {
|
|
|
7621
7650
|
provenance: "verified",
|
|
7622
7651
|
description: "Lock signal strength (verified: param 1141 = RSSI)."
|
|
7623
7652
|
},
|
|
7624
|
-
/**
|
|
7653
|
+
/**
|
|
7654
|
+
* Lock the deadbolt/door. Resolves after sending, without confirming physical state — except on the
|
|
7655
|
+
* classic Wi-Fi lock, which answers: there it rejects on a device refusal or no reply as well.
|
|
7656
|
+
*/
|
|
7625
7657
|
lock: method(({ ctx, sink }) => () => actuate(true, ctx, sink), "Lock the deadbolt/door.", actuationAvailable),
|
|
7626
|
-
/**
|
|
7658
|
+
/**
|
|
7659
|
+
* Unlock the deadbolt/door. Resolves after sending, without confirming physical state — except on the
|
|
7660
|
+
* classic Wi-Fi lock, which answers: there it rejects on a device refusal or no reply as well.
|
|
7661
|
+
*/
|
|
7627
7662
|
unlock: method(({ ctx, sink }) => () => actuate(false, ctx, sink), "Unlock the deadbolt/door.", actuationAvailable),
|
|
7628
7663
|
/**
|
|
7629
7664
|
* Read-modify-write the auto-lock setting: the transport GETs the device's current settings, changes
|
|
@@ -7645,13 +7680,13 @@ var LOCK_MEMBERS = {
|
|
|
7645
7680
|
enabled,
|
|
7646
7681
|
delaySeconds
|
|
7647
7682
|
});
|
|
7648
|
-
}, "Read-modify-write the auto-lock setting, preserving the fields it does not change.", (ctx) => !isVideoSmartLock(ctx)),
|
|
7683
|
+
}, "Read-modify-write the auto-lock setting, preserving the fields it does not change.", (ctx) => !isVideoSmartLock(ctx) && speaksFf09Settings(ctx)),
|
|
7649
7684
|
/**
|
|
7650
7685
|
* Toggle Rain Mode — a pure blind write, unlike `setAutoLock`: the compact frame carries only this one
|
|
7651
7686
|
* field, so there is no GET pass and it is fire-and-forget. Confirmed on-device end-to-end in both
|
|
7652
7687
|
* directions, with the app UI reflecting the new state afterward. P2P video lock only.
|
|
7653
7688
|
*/
|
|
7654
|
-
setRainMode: method(({ ctx, sink }) => (enabled) => settingToggle(LOCK_SETTING_ID.RAIN_MODE, "setRainMode", enabled, ctx, sink), "Toggle Rain Mode.",
|
|
7689
|
+
setRainMode: method(({ ctx, sink }) => (enabled) => settingToggle(LOCK_SETTING_ID.RAIN_MODE, "setRainMode", enabled, ctx, sink), "Toggle Rain Mode.", rainModeAvailable),
|
|
7655
7690
|
/**
|
|
7656
7691
|
* Lock the door by a single touch on the pad, with no code. First of the six compact toggles: all
|
|
7657
7692
|
* `unverified` (frame shape read out of the app's JS, never captured), so no setter is installed and
|
|
@@ -15140,13 +15175,8 @@ function abortable(work, signal) {
|
|
|
15140
15175
|
]);
|
|
15141
15176
|
}
|
|
15142
15177
|
|
|
15143
|
-
// dist/transport/p2p/
|
|
15144
|
-
import {
|
|
15145
|
-
|
|
15146
|
-
// dist/transport/p2p/p2p-session.js
|
|
15147
|
-
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
15148
|
-
import dgram from "node:dgram";
|
|
15149
|
-
import { constants as constants2, createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv4, generateKeyPairSync, privateDecrypt as privateDecrypt2, randomBytes as randomBytes2 } from "node:crypto";
|
|
15178
|
+
// dist/transport/p2p/keyed-payload.js
|
|
15179
|
+
import { createCipheriv as createCipheriv3, randomBytes as randomBytes3 } from "node:crypto";
|
|
15150
15180
|
|
|
15151
15181
|
// dist/transport/p2p/codec.js
|
|
15152
15182
|
var codec_exports = {};
|
|
@@ -15173,6 +15203,7 @@ __export(codec_exports, {
|
|
|
15173
15203
|
deriveLevel2KeyFromGatewayInfo: () => deriveLevel2KeyFromGatewayInfo,
|
|
15174
15204
|
deriveLevel2KeyFromRsaGatewayInfo: () => deriveLevel2KeyFromRsaGatewayInfo,
|
|
15175
15205
|
eciesUnwrap: () => eciesUnwrap,
|
|
15206
|
+
eciesWrap: () => eciesWrap,
|
|
15176
15207
|
encryptP2PData: () => encryptP2PData,
|
|
15177
15208
|
frameMessage: () => frameMessage,
|
|
15178
15209
|
gatewayInfoCipherId: () => gatewayInfoCipherId,
|
|
@@ -15185,7 +15216,7 @@ __export(codec_exports, {
|
|
|
15185
15216
|
readNullTerminatedString: () => readNullTerminatedString,
|
|
15186
15217
|
stringWithLength: () => stringWithLength
|
|
15187
15218
|
});
|
|
15188
|
-
import { constants, createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv3, createECDH as createECDH2, createHmac as createHmac2, createPrivateKey, privateDecrypt, timingSafeEqual } from "node:crypto";
|
|
15219
|
+
import { constants, createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv3, createECDH as createECDH2, createHmac as createHmac2, createPrivateKey, privateDecrypt, randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
|
|
15189
15220
|
var MAGIC_WORD = "XZYH";
|
|
15190
15221
|
var RequestMessageType = {
|
|
15191
15222
|
LOCAL_LOOKUP: Buffer.from([241, 48]),
|
|
@@ -15322,6 +15353,19 @@ function eciesUnwrap(envelope, eccPrivateKeyHex, options = {}) {
|
|
|
15322
15353
|
return void 0;
|
|
15323
15354
|
}
|
|
15324
15355
|
}
|
|
15356
|
+
function eciesWrap(plaintext, peerPublicKey, options = {}) {
|
|
15357
|
+
const ecdh = createECDH2("prime256v1");
|
|
15358
|
+
if (options.ephemeralPrivateKey)
|
|
15359
|
+
ecdh.setPrivateKey(options.ephemeralPrivateKey);
|
|
15360
|
+
else
|
|
15361
|
+
ecdh.generateKeys();
|
|
15362
|
+
const kdf = eufyKdf(ecdh.computeSecret(peerPublicKey), 48);
|
|
15363
|
+
const iv = options.iv ?? randomBytes2(16);
|
|
15364
|
+
const cipher = createCipheriv2("aes-128-cbc", kdf.subarray(0, 16), iv);
|
|
15365
|
+
const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
15366
|
+
const tag2 = createHmac2("sha256", kdf.subarray(16, 48)).update(iv).update(ct).digest();
|
|
15367
|
+
return Buffer.concat([ecdh.getPublicKey(null, "compressed"), iv, ct, tag2]);
|
|
15368
|
+
}
|
|
15325
15369
|
function deriveLevel2KeyFromGatewayInfo(gwPayload, eccPrivateKeyHex) {
|
|
15326
15370
|
const plain = eciesUnwrap(gwPayload.subarray(4, 4 + 129), eccPrivateKeyHex);
|
|
15327
15371
|
if (!plain)
|
|
@@ -16041,6 +16085,65 @@ function commandName(id) {
|
|
|
16041
16085
|
return COMMAND_NAMES[id] ?? `CMD_${id}`;
|
|
16042
16086
|
}
|
|
16043
16087
|
|
|
16088
|
+
// dist/transport/p2p/keyed-payload.js
|
|
16089
|
+
var KEYED_PAYLOAD_SEQ_ERROR = -151;
|
|
16090
|
+
function serialIv(deviceSn) {
|
|
16091
|
+
const iv = Buffer.alloc(16);
|
|
16092
|
+
Buffer.from(deviceSn, "utf8").copy(iv, 0, 0, 16);
|
|
16093
|
+
return iv;
|
|
16094
|
+
}
|
|
16095
|
+
function spellAesKey(key) {
|
|
16096
|
+
return key.toString("hex").toUpperCase();
|
|
16097
|
+
}
|
|
16098
|
+
function buildKeyBlob(aesKeyHex, devicePublicKeyHex, rnd = {}) {
|
|
16099
|
+
const devicePublicKey = Buffer.concat([Buffer.from([4]), Buffer.from(devicePublicKeyHex, "hex")]);
|
|
16100
|
+
return eciesWrap(Buffer.from(aesKeyHex, "utf8"), devicePublicKey, {
|
|
16101
|
+
ephemeralPrivateKey: rnd.ephemeralPrivateKey,
|
|
16102
|
+
iv: rnd.nonce
|
|
16103
|
+
}).toString("hex");
|
|
16104
|
+
}
|
|
16105
|
+
function sealInnerPayload(innerJson, aesKeyHex, deviceSn) {
|
|
16106
|
+
const cipher = createCipheriv3("aes-128-cbc", Buffer.from(aesKeyHex, "hex"), serialIv(deviceSn));
|
|
16107
|
+
return Buffer.concat([cipher.update(Buffer.from(innerJson, "utf8")), cipher.final()]).toString("base64");
|
|
16108
|
+
}
|
|
16109
|
+
function buildKeyedActuatePayload(id, devicePublicKeyHex, userName, rnd) {
|
|
16110
|
+
const aesKeyHex = spellAesKey(rnd.aesKey ?? randomBytes3(16));
|
|
16111
|
+
const inner = JSON.stringify({
|
|
16112
|
+
shortUserId: id.shortUserId,
|
|
16113
|
+
slOperation: id.engage ? 1 : 0,
|
|
16114
|
+
userId: id.adminUserId,
|
|
16115
|
+
userName,
|
|
16116
|
+
seq_num: rnd.seqNum
|
|
16117
|
+
});
|
|
16118
|
+
return {
|
|
16119
|
+
key: buildKeyBlob(aesKeyHex, devicePublicKeyHex, rnd),
|
|
16120
|
+
payload: sealInnerPayload(inner, aesKeyHex, id.deviceSn)
|
|
16121
|
+
};
|
|
16122
|
+
}
|
|
16123
|
+
function keyedPayloadReportCode(json, engage) {
|
|
16124
|
+
if (json?.cmd !== CommandType.P2P_QUERY_STATUS_IN_LOCK)
|
|
16125
|
+
return void 0;
|
|
16126
|
+
const payload = json.payload;
|
|
16127
|
+
const slState = engage ? "4" : "3";
|
|
16128
|
+
if (payload?.slState !== slState)
|
|
16129
|
+
return void 0;
|
|
16130
|
+
return typeof payload?.code === "number" ? payload.code : void 0;
|
|
16131
|
+
}
|
|
16132
|
+
function keyedPayloadSeqMark(json) {
|
|
16133
|
+
if (json?.cmd !== CommandType.CMD_DOORLOCK_P2P_SEQ)
|
|
16134
|
+
return void 0;
|
|
16135
|
+
const seq = json.payload?.seq_num;
|
|
16136
|
+
return typeof seq === "number" && Number.isFinite(seq) ? seq : void 0;
|
|
16137
|
+
}
|
|
16138
|
+
|
|
16139
|
+
// dist/transport/p2p/command-router.js
|
|
16140
|
+
import { setTimeout as sleep2 } from "node:timers/promises";
|
|
16141
|
+
|
|
16142
|
+
// dist/transport/p2p/p2p-session.js
|
|
16143
|
+
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
16144
|
+
import dgram from "node:dgram";
|
|
16145
|
+
import { constants as constants2, createCipheriv as createCipheriv4, createDecipheriv as createDecipheriv4, generateKeyPairSync, privateDecrypt as privateDecrypt2, randomBytes as randomBytes4 } from "node:crypto";
|
|
16146
|
+
|
|
16044
16147
|
// dist/transport/p2p/live-trace.js
|
|
16045
16148
|
var LIVE_TRACE_MESSAGE = "[live] start trace";
|
|
16046
16149
|
function traceLiveStart(logger, trace, source) {
|
|
@@ -17449,8 +17552,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17449
17552
|
encryptLevel2(plaintext) {
|
|
17450
17553
|
if (!this.level2Key)
|
|
17451
17554
|
return void 0;
|
|
17452
|
-
const nonce =
|
|
17453
|
-
const c =
|
|
17555
|
+
const nonce = randomBytes4(12);
|
|
17556
|
+
const c = createCipheriv4("aes-256-gcm", this.level2Key, nonce);
|
|
17454
17557
|
c.setAAD(GCM_AAD);
|
|
17455
17558
|
const ct = Buffer.concat([c.update(plaintext), c.final()]);
|
|
17456
17559
|
const sub2 = Buffer.alloc(4);
|
|
@@ -17660,22 +17763,28 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17660
17763
|
* Low-level: send a `CMD_SET_PAYLOAD` (1350) wrapping `{account_id, cmd:<subCmd>, mChannel,
|
|
17661
17764
|
* payload, transaction}` over the level-1 channel. The reply arrives as a `NOTIFY_PAYLOAD`
|
|
17662
17765
|
* (1351) frame (level-1 decrypted, surfaced via the `data` event / `frame.json`).
|
|
17766
|
+
*
|
|
17767
|
+
* `opts.key` makes it a keyed envelope: the sealed per-command key rides ahead of the other fields
|
|
17768
|
+
* as the value's `key`, and the value goes out unencrypted, encryption-type 0, with `=` written
|
|
17769
|
+
* `=` — the form the vendor app sends one in, and the one form the device reads it in.
|
|
17663
17770
|
*/
|
|
17664
17771
|
sendSetPayload(subCmd, payload = {}, opts = {}) {
|
|
17665
17772
|
if (!this.connectAddress)
|
|
17666
17773
|
throw new Error("not connected");
|
|
17667
17774
|
const channel = opts.channel ?? 0;
|
|
17775
|
+
const keyed = opts.key !== void 0;
|
|
17668
17776
|
const inner = opts.rawValue ?? {
|
|
17777
|
+
...keyed ? { key: opts.key } : {},
|
|
17669
17778
|
account_id: opts.accountId ?? "",
|
|
17670
17779
|
cmd: subCmd,
|
|
17671
17780
|
mChannel: channel,
|
|
17672
17781
|
mValue3: 0,
|
|
17673
17782
|
...opts.omitPayload ? {} : { payload }
|
|
17674
17783
|
};
|
|
17675
|
-
const value = JSON.stringify(inner);
|
|
17784
|
+
const value = keyed ? JSON.stringify(inner).replace(/=/g, "\\u003d") : JSON.stringify(inner);
|
|
17676
17785
|
const body = Buffer.concat([
|
|
17677
17786
|
buildCommandHeader(this.seqNumber, opts.wrapCmd ?? CMD_SET_PAYLOAD),
|
|
17678
|
-
buildStringCommandPayload(value, channel, this.level1Key, 1)
|
|
17787
|
+
buildStringCommandPayload(value, channel, keyed ? void 0 : this.level1Key, 1)
|
|
17679
17788
|
]);
|
|
17680
17789
|
this.seqNumber = this.seqNumber + 1 & 65535;
|
|
17681
17790
|
this.logger.debug(`[p2p] ${this.cfg.stationSn} sendSetPayload subCmd=${subCmd}`);
|
|
@@ -18026,7 +18135,7 @@ function buildDeviceNameBody(channel, name, accountId) {
|
|
|
18026
18135
|
}
|
|
18027
18136
|
|
|
18028
18137
|
// dist/transport/ff09.js
|
|
18029
|
-
import { createCipheriv as
|
|
18138
|
+
import { createCipheriv as createCipheriv5, createDecipheriv as createDecipheriv5 } from "node:crypto";
|
|
18030
18139
|
var LOCK_API_COMMAND = {
|
|
18031
18140
|
/** Lock / unlock the deadbolt — video lock AND garage door, both directions (see above). */
|
|
18032
18141
|
ON_OFF_LOCK: 6018,
|
|
@@ -18106,7 +18215,7 @@ function assembleFf09Frame(commandCode, apiCommand, plain, adminUserId, deviceSn
|
|
|
18106
18215
|
const key = Buffer.concat([Buffer.from(adminUserId.slice(-12), "ascii"), u32be(keyTime)]);
|
|
18107
18216
|
const iv = Buffer.alloc(16);
|
|
18108
18217
|
Buffer.from(deviceSn, "ascii").copy(iv);
|
|
18109
|
-
const cipher =
|
|
18218
|
+
const cipher = createCipheriv5("aes-128-cbc", key, iv);
|
|
18110
18219
|
const ct = Buffer.concat([cipher.update(plain), cipher.final()]);
|
|
18111
18220
|
const body = Buffer.concat([Buffer.from([3, 0, 2]), u16be(16384 | commandCode), ct]);
|
|
18112
18221
|
const size = 2 + 2 + body.length + 1;
|
|
@@ -21814,6 +21923,17 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
21814
21923
|
cipherKeyCache = /* @__PURE__ */ new Map();
|
|
21815
21924
|
/** The recording download in flight per station; a station serves one at a time. */
|
|
21816
21925
|
recordingDownloads = /* @__PURE__ */ new Map();
|
|
21926
|
+
/**
|
|
21927
|
+
* The `seq_num` last sent to each device over the keyed-payload wire. The device holds a mark for the life
|
|
21928
|
+
* of a P2P connection: the first keyed command on a new connection is accepted at whatever number it
|
|
21929
|
+
* carries and becomes the mark, and after that a number not above the mark is refused with the mark
|
|
21930
|
+
* published ({@link keyedPayloadSeqMark}). Each accepted command moves this to its number, a refusal
|
|
21931
|
+
* moves it to the published mark + 1, and a device absent here has not been sent one in this process and
|
|
21932
|
+
* seeds from a small random number — which a new connection takes as sent.
|
|
21933
|
+
*/
|
|
21934
|
+
keyedSequences = /* @__PURE__ */ new Map();
|
|
21935
|
+
/** The keyed-payload command in flight per device, which the next one for that device waits on. */
|
|
21936
|
+
keyedQueue = /* @__PURE__ */ new Map();
|
|
21817
21937
|
constructor(deps) {
|
|
21818
21938
|
this.deps = deps;
|
|
21819
21939
|
this.manager = new SessionManager({
|
|
@@ -22169,6 +22289,9 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22169
22289
|
case "ff09-actuate":
|
|
22170
22290
|
await this.sendFf09Actuate(sn, cmd);
|
|
22171
22291
|
return;
|
|
22292
|
+
case "keyed-payload-actuate":
|
|
22293
|
+
await this.sendKeyedPayloadActuate(sn, cmd);
|
|
22294
|
+
return;
|
|
22172
22295
|
case "ff09-autolock":
|
|
22173
22296
|
await this.sendFf09Autolock(sn, cmd);
|
|
22174
22297
|
return;
|
|
@@ -23151,6 +23274,163 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
23151
23274
|
}
|
|
23152
23275
|
await this.replayLevel2Send(sn, `set-payload cmd ${cmd}`, ({ session, accountId }) => session.sendControlLevel2(cmd, channel, accountId, payload, mValue3 ?? cmd), resolved ?? await this.resolveSession(sn, { waitLevel2: true, level2Sender }));
|
|
23153
23276
|
}
|
|
23277
|
+
/**
|
|
23278
|
+
* **`keyed-payload-actuate` intent** — the classic Wi-Fi lock's actuation: the sealed fields from
|
|
23279
|
+
* {@link buildKeyedActuatePayload}, sent as a keyed envelope through {@link P2PSession.sendSetPayload}
|
|
23280
|
+
* on the lock's device channel. No session key is waited for — the envelope seals its own per-command
|
|
23281
|
+
* key for the device's public key (fetched from the cloud, cached per serial), so it goes as soon as
|
|
23282
|
+
* the session is up. The device shows the acting member by display name, which lives on the device
|
|
23283
|
+
* record's `member` rather than in the intent, so it is read here and falls back to the account name
|
|
23284
|
+
* the intent carries.
|
|
23285
|
+
*
|
|
23286
|
+
* The device answers, so the outcome is read rather than assumed: {@link awaitKeyedReply} settles each
|
|
23287
|
+
* send on the replies, and resolves a {@link KEYED_PAYLOAD_SEQ_ERROR} refusal with the mark the device
|
|
23288
|
+
* publishes in the refusal's `CMD_DOORLOCK_P2P_SEQ` (1916) twin. On that mark the command resends once,
|
|
23289
|
+
* at mark + 1; a second refusal, or a refusal that published no mark, throws. Commands run one at a time
|
|
23290
|
+
* per device: the replies carry no correlation id, so one command's answer must never be read as
|
|
23291
|
+
* another's.
|
|
23292
|
+
*/
|
|
23293
|
+
sendKeyedPayloadActuate(sn, cmd) {
|
|
23294
|
+
return this.serializeKeyed(sn, async () => {
|
|
23295
|
+
const dev = await this.deviceFor(sn);
|
|
23296
|
+
const member = (dev.raw ?? {}).member;
|
|
23297
|
+
const userName = typeof member?.nick_name === "string" && member.nick_name ? member.nick_name : cmd.username;
|
|
23298
|
+
const devicePublicKey = await this.deps.mega.getDevicePublicKey(sn);
|
|
23299
|
+
const resolved = await this.resolveSession(sn, { waitLevel2: false });
|
|
23300
|
+
const send = (seqNum) => {
|
|
23301
|
+
const { key, payload } = buildKeyedActuatePayload(cmd, devicePublicKey, userName, { seqNum });
|
|
23302
|
+
resolved.session.sendSetPayload(CommandType.P2P_ON_OFF_LOCK, payload, {
|
|
23303
|
+
accountId: cmd.adminUserId,
|
|
23304
|
+
channel: resolved.channel,
|
|
23305
|
+
key
|
|
23306
|
+
});
|
|
23307
|
+
};
|
|
23308
|
+
await this.sendKeyedAtSequence(sn, resolved.session, resolved.channel, cmd.engage, send);
|
|
23309
|
+
});
|
|
23310
|
+
}
|
|
23311
|
+
/** Run `work` once every keyed-payload command already queued for `sn` has settled. */
|
|
23312
|
+
serializeKeyed(sn, work) {
|
|
23313
|
+
const previous = this.keyedQueue.get(sn) ?? Promise.resolve();
|
|
23314
|
+
const run = previous.then(work);
|
|
23315
|
+
const settled = run.then(() => void 0, () => void 0);
|
|
23316
|
+
this.keyedQueue.set(sn, settled);
|
|
23317
|
+
void settled.then(() => {
|
|
23318
|
+
if (this.keyedQueue.get(sn) === settled)
|
|
23319
|
+
this.keyedQueue.delete(sn);
|
|
23320
|
+
});
|
|
23321
|
+
return run;
|
|
23322
|
+
}
|
|
23323
|
+
/**
|
|
23324
|
+
* Send one keyed command at the device's next sequence and, on a sequence refusal, resend once at the
|
|
23325
|
+
* mark the refusal published.
|
|
23326
|
+
*
|
|
23327
|
+
* A device's first number in a process is a small random seed — the scale the vendor app's own counters
|
|
23328
|
+
* run at — and every later one is the previous plus one. The device takes the first number on a new
|
|
23329
|
+
* connection as sent, and from then on refuses one not above the mark it holds, stating the mark in the
|
|
23330
|
+
* refusal's `CMD_DOORLOCK_P2P_SEQ` (1916) twin ({@link awaitKeyedReply} resolves the refusal with it).
|
|
23331
|
+
* So a refusal is resolved to that mark and the command resent once at mark + 1, the number the device
|
|
23332
|
+
* accepts after a refusal; a second refusal, or a refusal that carried no mark, throws. The accepted
|
|
23333
|
+
* number is recorded, so the next command starts just above it.
|
|
23334
|
+
*
|
|
23335
|
+
* @param engage whether the command drives to the engaged (locked) state, which its own report states.
|
|
23336
|
+
* @param send dispatches the envelope at the sequence it is given.
|
|
23337
|
+
*/
|
|
23338
|
+
async sendKeyedAtSequence(sn, session, channel, engage, send) {
|
|
23339
|
+
const last = this.keyedSequences.get(sn);
|
|
23340
|
+
const first = last === void 0 ? Math.trunc(Math.random() * 1e3) : last + 1;
|
|
23341
|
+
this.keyedSequences.set(sn, first);
|
|
23342
|
+
const reply = await this.awaitKeyedReply(sn, session, channel, engage, () => send(first));
|
|
23343
|
+
if (reply.code === 0)
|
|
23344
|
+
return;
|
|
23345
|
+
if (reply.code !== KEYED_PAYLOAD_SEQ_ERROR || reply.mark === void 0) {
|
|
23346
|
+
throw new Error(`keyed-payload command rejected by ${sn} with code ${reply.code}`);
|
|
23347
|
+
}
|
|
23348
|
+
const resend = reply.mark + 1;
|
|
23349
|
+
this.keyedSequences.set(sn, resend);
|
|
23350
|
+
const second = await this.awaitKeyedReply(sn, session, channel, engage, () => send(resend));
|
|
23351
|
+
if (second.code === 0)
|
|
23352
|
+
return;
|
|
23353
|
+
throw new Error(`keyed-payload command rejected by ${sn} with code ${second.code}`);
|
|
23354
|
+
}
|
|
23355
|
+
/**
|
|
23356
|
+
* Send one keyed-payload command and read the device's answer for it from the replies on the command's
|
|
23357
|
+
* channel: the `SET_PAYLOAD` echo (a `commandResult`), at once, carrying the result as an int32 — or,
|
|
23358
|
+
* when the echo was not read and the command changed the device's state, the `NOTIFY_PAYLOAD` about two
|
|
23359
|
+
* seconds later carrying the result again as a `P2P_QUERY_STATUS_IN_LOCK` (1955) document. A report
|
|
23360
|
+
* is tied to the command by its `slState`: only one stating the state this command drives to, by
|
|
23361
|
+
* `engage` ({@link keyedPayloadReportCode}), is read as its answer. A report of the other state is a
|
|
23362
|
+
* previous command's, landing after that command settled on its echo, and is ignored.
|
|
23363
|
+
*
|
|
23364
|
+
* - An accepted result (code `0`) settles the command.
|
|
23365
|
+
* - A sequence refusal is stated twice, both about a tenth of a second after the send: the `-151` echo
|
|
23366
|
+
* and a `CMD_DOORLOCK_P2P_SEQ` (1916) notify whose `seq_num` is the mark the device holds. The
|
|
23367
|
+
* resolved value carries that mark, so the caller resends just above it. Collection runs for
|
|
23368
|
+
* {@link KEYED_REFUSAL_TWIN_MS} after the first of the two, so both land and neither is left on the
|
|
23369
|
+
* channel for the resend to read as its own answer, and either one alone still resolves the refusal.
|
|
23370
|
+
* - No reply within {@link KEYED_REPLY_MS} is a command that never landed, and rejects.
|
|
23371
|
+
*
|
|
23372
|
+
* Both listeners are armed before the send, and only replies on `channel` are read. The channel names
|
|
23373
|
+
* the device, not the command: every other P2P write a lock takes waits for a level-2 key this device
|
|
23374
|
+
* never negotiates, so nothing else the SDK sends can draw a reply here, and a future intent on this
|
|
23375
|
+
* wrapper has to share {@link serializeKeyed}.
|
|
23376
|
+
*/
|
|
23377
|
+
awaitKeyedReply(sn, session, channel, engage, send) {
|
|
23378
|
+
return new Promise((resolve, reject) => {
|
|
23379
|
+
let refused = false;
|
|
23380
|
+
let mark;
|
|
23381
|
+
let timer;
|
|
23382
|
+
const settle = (outcome) => {
|
|
23383
|
+
if (timer)
|
|
23384
|
+
clearTimeout(timer);
|
|
23385
|
+
session.off("commandResult", onResult);
|
|
23386
|
+
session.off("data", onData);
|
|
23387
|
+
outcome();
|
|
23388
|
+
};
|
|
23389
|
+
const expire = (ms, onExpiry) => {
|
|
23390
|
+
if (timer)
|
|
23391
|
+
clearTimeout(timer);
|
|
23392
|
+
timer = setTimeout(onExpiry, ms);
|
|
23393
|
+
};
|
|
23394
|
+
const refuse = () => {
|
|
23395
|
+
if (refused)
|
|
23396
|
+
return;
|
|
23397
|
+
refused = true;
|
|
23398
|
+
expire(_P2PCommandRouter.KEYED_REFUSAL_TWIN_MS, () => settle(() => resolve({ code: KEYED_PAYLOAD_SEQ_ERROR, mark })));
|
|
23399
|
+
};
|
|
23400
|
+
const onResult = (r) => {
|
|
23401
|
+
if (r.channel !== channel)
|
|
23402
|
+
return;
|
|
23403
|
+
if (r.code === KEYED_PAYLOAD_SEQ_ERROR)
|
|
23404
|
+
refuse();
|
|
23405
|
+
else if (!refused)
|
|
23406
|
+
settle(() => resolve({ code: r.code }));
|
|
23407
|
+
};
|
|
23408
|
+
const onData = (f) => {
|
|
23409
|
+
if (f.channel !== channel || f.commandId !== CommandType.CMD_NOTIFY_PAYLOAD)
|
|
23410
|
+
return;
|
|
23411
|
+
const published = keyedPayloadSeqMark(f.json);
|
|
23412
|
+
if (published !== void 0) {
|
|
23413
|
+
mark = published;
|
|
23414
|
+
return refuse();
|
|
23415
|
+
}
|
|
23416
|
+
const code = keyedPayloadReportCode(f.json, engage);
|
|
23417
|
+
if (code !== void 0 && !refused)
|
|
23418
|
+
settle(() => resolve({ code }));
|
|
23419
|
+
};
|
|
23420
|
+
session.on("commandResult", onResult);
|
|
23421
|
+
session.on("data", onData);
|
|
23422
|
+
expire(_P2PCommandRouter.KEYED_REPLY_MS, () => settle(() => reject(new Error(`keyed-payload command to ${sn} drew no reply`))));
|
|
23423
|
+
try {
|
|
23424
|
+
send();
|
|
23425
|
+
} catch (e) {
|
|
23426
|
+
settle(() => reject(e));
|
|
23427
|
+
}
|
|
23428
|
+
});
|
|
23429
|
+
}
|
|
23430
|
+
/** How long a keyed-payload command waits for any reply before it is taken as never having landed. */
|
|
23431
|
+
static KEYED_REPLY_MS = 6e3;
|
|
23432
|
+
/** How long a refusal collects its two statements — the `-151` echo and the mark notify — before resolving. */
|
|
23433
|
+
static KEYED_REFUSAL_TWIN_MS = 500;
|
|
23154
23434
|
/**
|
|
23155
23435
|
* **`ff09-actuate` intent** — build the `ff09` AES-128-CBC frame ({@link buildFf09Frame}, shared with the
|
|
23156
23436
|
* MQTT transport — see `transport/ff09.ts`) and dispatch it in the `1940` TRANSFER_PAYLOAD envelope
|
|
@@ -25288,7 +25568,7 @@ var RtcCommandRouter = class {
|
|
|
25288
25568
|
};
|
|
25289
25569
|
|
|
25290
25570
|
// dist/transport/mqtt/command-router.js
|
|
25291
|
-
import { randomBytes as
|
|
25571
|
+
import { randomBytes as randomBytes5 } from "node:crypto";
|
|
25292
25572
|
|
|
25293
25573
|
// dist/transport/mqtt/broker-discovery.js
|
|
25294
25574
|
import { resolve4 } from "node:dns/promises";
|
|
@@ -25671,9 +25951,9 @@ function buildFf09MqttEnvelope(input) {
|
|
|
25671
25951
|
const head = {
|
|
25672
25952
|
version: "1.0.0.1",
|
|
25673
25953
|
client_id: input.clientId,
|
|
25674
|
-
sess_id: input.sessId ??
|
|
25954
|
+
sess_id: input.sessId ?? randomBytes5(2).toString("hex"),
|
|
25675
25955
|
msg_seq: 1,
|
|
25676
|
-
seed: input.seed ??
|
|
25956
|
+
seed: input.seed ?? randomBytes5(16).toString("hex"),
|
|
25677
25957
|
timestamp: input.timestamp ?? Math.floor(Date.now() / 1e3),
|
|
25678
25958
|
cmd_status: 2,
|
|
25679
25959
|
cmd: 9,
|
|
@@ -26134,7 +26414,7 @@ var MqttCommandRouter = class _MqttCommandRouter {
|
|
|
26134
26414
|
};
|
|
26135
26415
|
|
|
26136
26416
|
// dist/transport/tuya/account.js
|
|
26137
|
-
import { createCipheriv as
|
|
26417
|
+
import { createCipheriv as createCipheriv6 } from "node:crypto";
|
|
26138
26418
|
var TUYA_USERNAME_PREFIX = "eh-";
|
|
26139
26419
|
var TUYA_PASSWORD_KEY = Buffer.from([
|
|
26140
26420
|
36,
|
|
@@ -26183,7 +26463,7 @@ function zeroPadTo16(s) {
|
|
|
26183
26463
|
return Buffer.concat([Buffer.from("0".repeat(16 - rem), "utf-8"), buf]);
|
|
26184
26464
|
}
|
|
26185
26465
|
function deriveTuyaPassword(eufyUserId) {
|
|
26186
|
-
const cipher =
|
|
26466
|
+
const cipher = createCipheriv6("aes-128-cbc", TUYA_PASSWORD_KEY, TUYA_PASSWORD_IV);
|
|
26187
26467
|
cipher.setAutoPadding(false);
|
|
26188
26468
|
const ct = Buffer.concat([cipher.update(zeroPadTo16(tuyaUsername(eufyUserId))), cipher.final()]);
|
|
26189
26469
|
return ct.toString("hex").toUpperCase();
|
|
@@ -26338,10 +26618,10 @@ var HmacSigner = class {
|
|
|
26338
26618
|
var TUYA_CHKEY = "7cbfe6d8";
|
|
26339
26619
|
|
|
26340
26620
|
// dist/transport/tuya/client.js
|
|
26341
|
-
import { randomBytes as
|
|
26621
|
+
import { randomBytes as randomBytes7, createHash as createHash6, createPublicKey, publicEncrypt, constants as constants3 } from "node:crypto";
|
|
26342
26622
|
|
|
26343
26623
|
// dist/transport/tuya/et3.js
|
|
26344
|
-
import { createCipheriv as
|
|
26624
|
+
import { createCipheriv as createCipheriv7, createDecipheriv as createDecipheriv8, createHmac as createHmac5, randomBytes as randomBytes6 } from "node:crypto";
|
|
26345
26625
|
import { gunzipSync } from "node:zlib";
|
|
26346
26626
|
function deriveBodyKey(requestId, ecode) {
|
|
26347
26627
|
const k = resolveSignKey();
|
|
@@ -26350,8 +26630,8 @@ function deriveBodyKey(requestId, ecode) {
|
|
|
26350
26630
|
return Buffer.from(hex.slice(0, 16), "ascii");
|
|
26351
26631
|
}
|
|
26352
26632
|
function encryptBody2(key, json) {
|
|
26353
|
-
const nonce =
|
|
26354
|
-
const cipher =
|
|
26633
|
+
const nonce = randomBytes6(12);
|
|
26634
|
+
const cipher = createCipheriv7("aes-128-gcm", key, nonce);
|
|
26355
26635
|
const ciphertext = Buffer.concat([cipher.update(Buffer.from(json, "utf-8")), cipher.final()]);
|
|
26356
26636
|
return Buffer.concat([nonce, ciphertext, cipher.getAuthTag()]).toString("base64");
|
|
26357
26637
|
}
|
|
@@ -26489,7 +26769,7 @@ function buildUidTokenCreateAction(countryCode, uid) {
|
|
|
26489
26769
|
|
|
26490
26770
|
// dist/transport/tuya/client.js
|
|
26491
26771
|
function genDeviceId() {
|
|
26492
|
-
return
|
|
26772
|
+
return randomBytes7(22).toString("hex");
|
|
26493
26773
|
}
|
|
26494
26774
|
function rsaEncryptPassword(password, publicKeyDecimal, exponentDecimal) {
|
|
26495
26775
|
const bigintToBuffer = (n) => {
|
|
@@ -27611,7 +27891,7 @@ var PushClient = class _PushClient extends EventEmitter13 {
|
|
|
27611
27891
|
};
|
|
27612
27892
|
|
|
27613
27893
|
// dist/transport/push/fcm.js
|
|
27614
|
-
import { randomBytes as
|
|
27894
|
+
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
27615
27895
|
import { setTimeout as sleep3 } from "node:timers/promises";
|
|
27616
27896
|
var FCM = {
|
|
27617
27897
|
PROJECT_ID: "batterycam-3250a",
|
|
@@ -27624,7 +27904,7 @@ var FCM = {
|
|
|
27624
27904
|
SDK_VERSION: "a:16.3.1"
|
|
27625
27905
|
};
|
|
27626
27906
|
function generateFid() {
|
|
27627
|
-
const b =
|
|
27907
|
+
const b = randomBytes8(17);
|
|
27628
27908
|
b[0] = 112 + b[0] % 16;
|
|
27629
27909
|
return b.toString("base64url").slice(0, 22);
|
|
27630
27910
|
}
|
|
@@ -29542,8 +29822,10 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
29542
29822
|
* Choose the transport stack for one command — the routing half of {@link commandSinkFor}.
|
|
29543
29823
|
*
|
|
29544
29824
|
* The `ff09-*` kinds share ONE frame that rides either transport, so each stack is asked whether it
|
|
29545
|
-
* drives this device (`claimsDevice`) rather than inferring from the kind
|
|
29546
|
-
*
|
|
29825
|
+
* drives this device (`claimsDevice`) rather than inferring from the kind; `keyed-payload-actuate` (the
|
|
29826
|
+
* classic Wi-Fi lock) is routed the same way, a channel-less actuation claimed by the stack that drives
|
|
29827
|
+
* the lock. `registry.require` throws on an unloaded serial, so a routing decision never silently falls
|
|
29828
|
+
* through to the wrong transport.
|
|
29547
29829
|
* The `eufy_life` DP writes (smart lights) are secure-MQTT-only. `aiot-dp` routes to either the
|
|
29548
29830
|
* Anker AIoT MQTT stack or the legacy Tuya REST router depending on the device's category
|
|
29549
29831
|
* (`eufy_home_tuya` → Tuya, everything else → MQTT). The capability layer emits a single `aiot-dp`
|
|
@@ -29554,7 +29836,7 @@ var EufyMega = class extends EventEmitter14 {
|
|
|
29554
29836
|
* media only, and the RTC router refuses a command on it. Everything else is P2P.
|
|
29555
29837
|
*/
|
|
29556
29838
|
routeCommand(sn, cmd) {
|
|
29557
|
-
if (cmd.kind === "ff09-actuate" || cmd.kind === "ff09-autolock" || cmd.kind === "ff09-setting-toggle") {
|
|
29839
|
+
if (cmd.kind === "ff09-actuate" || cmd.kind === "ff09-autolock" || cmd.kind === "ff09-setting-toggle" || cmd.kind === "keyed-payload-actuate") {
|
|
29558
29840
|
const dev = this.registry.require(sn);
|
|
29559
29841
|
if (P2PCommandRouter.claimsDevice(dev))
|
|
29560
29842
|
return this.p2p.dispatchCommand(sn, cmd);
|