@ray-js/lock-sdk 1.2.0-beta.25 → 1.2.0-beta.26

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.
@@ -7,3 +7,4 @@ export declare function bindLockToFit(lock: UnbindLockItem): Promise<unknown>;
7
7
  export declare function unbindLockToFit(lock: BoundFitLockItem & {
8
8
  bindingId: string;
9
9
  }): Promise<unknown>;
10
+ export declare function activateLockToFit(lock: BoundFitLockItem): Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { getBoundLockListForRuntime, getUnbindLockListForRuntime, } from "../../../feature/fit/list";
2
- import { bindLockToFitForRuntime, unbindLockToFitForRuntime, } from "../../../feature/fit/bind-lock-to-fit";
2
+ import { activateLockToFitForRuntime, bindLockToFitForRuntime, unbindLockToFitForRuntime, } from "../../../feature/fit/bind-lock-to-fit";
3
3
  import { resolveRuntimeFromExportThis } from "../bootstrap/resolve-runtime-from-export-this";
4
4
  export function getBoundLockList() {
5
5
  return getBoundLockListForRuntime(resolveRuntimeFromExportThis(this));
@@ -13,3 +13,6 @@ export function bindLockToFit(lock) {
13
13
  export function unbindLockToFit(lock) {
14
14
  return unbindLockToFitForRuntime(resolveRuntimeFromExportThis(this), lock);
15
15
  }
16
+ export function activateLockToFit(lock) {
17
+ return activateLockToFitForRuntime(resolveRuntimeFromExportThis(this), lock);
18
+ }
@@ -2,3 +2,4 @@ import { BoundFitLockItem, LockFitItemBase } from "../../api/fit";
2
2
  import type { LockDeviceRuntime } from "../../runtime/LockDeviceRuntime";
3
3
  export declare function bindLockToFitForRuntime(runtime: LockDeviceRuntime, lock: LockFitItemBase): Promise<unknown>;
4
4
  export declare function unbindLockToFitForRuntime(runtime: LockDeviceRuntime, lock: BoundFitLockItem): Promise<unknown>;
5
+ export declare function activateLockToFitForRuntime(runtime: LockDeviceRuntime, lock: LockFitItemBase): Promise<void>;
@@ -1,9 +1,10 @@
1
1
  import { bindLockAndFit, unbindFitFromLockCloud, unbindLockFromFitCloud, } from "../../api/fit";
2
+ import { setDeviceProperties } from "../../api/setting";
2
3
  import { getUsersSyncLockData } from "../../api/user";
3
- import { getDeviceProperties, offDpDataChange, onDpDataChange, publishDeviceDps, setDeviceProperty, } from "../../platform/device";
4
+ import { offDpDataChange, onDpDataChange, publishDeviceDps, } from "../../platform/device";
4
5
  import { getError } from "../../utils/errors";
5
6
  import { sdkLog } from "../../utils/sdk-log";
6
- import { isFitSecurityChannelEnabledForRuntime } from "./security-channel";
7
+ import { getSecurityUpdatedLockIds, isFitSecurityChannelEnabledForRuntime, } from "./security-channel";
7
8
  import { FIT_BIND_TIMEOUT_MS, FIT_OPERATION_CODE, normalizeHex, buildLockCheckCodeTemplate, publishFitTransparentFlow, } from "./shared";
8
9
  const markSecurityUpdated = async (runtime, lock) => {
9
10
  if (!lock.isSupportScy || !isFitSecurityChannelEnabledForRuntime(runtime)) {
@@ -11,28 +12,15 @@ const markSecurityUpdated = async (runtime, lock) => {
11
12
  }
12
13
  const fittingId = runtime.devId;
13
14
  try {
14
- const props = await getDeviceProperties(fittingId);
15
- const raw = props?.["SECURITY_UPDATED"];
16
- let list = [];
17
- if (typeof raw === "string") {
18
- try {
19
- list = JSON.parse(raw).value || [];
20
- }
21
- catch {
22
- list = [];
23
- }
24
- }
25
- else if (raw && typeof raw === "object") {
26
- list =
27
- raw.value ||
28
- (Array.isArray(raw) ? raw : []);
29
- }
15
+ const list = await getSecurityUpdatedLockIds(fittingId);
30
16
  if (list.includes(lock.devId)) {
31
17
  return;
32
18
  }
33
- await setDeviceProperty(fittingId, "SECURITY_UPDATED", {
34
- key: fittingId,
35
- value: [...list, lock.devId],
19
+ await setDeviceProperties(fittingId, {
20
+ SECURITY_UPDATED: JSON.stringify({
21
+ key: fittingId,
22
+ value: [...list, lock.devId],
23
+ }),
36
24
  });
37
25
  }
38
26
  catch (e) {
@@ -205,3 +193,28 @@ export async function unbindLockToFitForRuntime(runtime, lock) {
205
193
  throw error;
206
194
  }
207
195
  }
196
+ export async function activateLockToFitForRuntime(runtime, lock) {
197
+ const startedAt = Date.now();
198
+ sdkLog("info", "fit", "activateLockToFitStart", {
199
+ devId: runtime.devId,
200
+ data: { lockDevId: lock.devId },
201
+ });
202
+ try {
203
+ await publishToFitting(runtime, "bind", lock);
204
+ await markSecurityUpdated(runtime, lock);
205
+ sdkLog("info", "fit", "activateLockToFitSuccess", {
206
+ devId: runtime.devId,
207
+ duration: Date.now() - startedAt,
208
+ data: { lockDevId: lock.devId },
209
+ });
210
+ }
211
+ catch (error) {
212
+ sdkLog("error", "fit", "activateLockToFitFail", {
213
+ devId: runtime.devId,
214
+ duration: Date.now() - startedAt,
215
+ data: { lockDevId: lock.devId },
216
+ error,
217
+ });
218
+ throw error;
219
+ }
220
+ }
@@ -7,6 +7,7 @@ export interface UnbindLockListItem extends UnbindLockItem {
7
7
  }
8
8
  export interface BoundLockListItem extends BoundFitLockItem {
9
9
  securityVersionMismatch: boolean;
10
+ securityNeedUpdate: boolean;
10
11
  }
11
12
  export declare function getBoundLockListForRuntime(runtime: LockDeviceRuntime): Promise<BoundLockListItem[]>;
12
13
  export declare function getUnbindLockListForRuntime(runtime: LockDeviceRuntime): Promise<UnbindLockListItem[]>;
@@ -1,7 +1,7 @@
1
1
  import { getBoundLockListApi, getUnbindLockListApi, } from "../../api/fit";
2
2
  import { sdkLog } from "../../utils/sdk-log";
3
3
  import { getFitDeviceBvForRuntime } from "./device-bv";
4
- import { isFitSecurityChannelEnabledForRuntime } from "./security-channel";
4
+ import { getSecurityUpdatedLockIds, isFitSecurityChannelEnabledForRuntime, } from "./security-channel";
5
5
  const isSecurityVersionMismatch = (runtime, lock) => !!lock.isSupportScy && !isFitSecurityChannelEnabledForRuntime(runtime);
6
6
  const resolveUnbindableReason = (runtime, lock, fitBv) => {
7
7
  const lockBv = Number(lock.protocolVersion) || 0;
@@ -19,9 +19,17 @@ export async function getBoundLockListForRuntime(runtime) {
19
19
  devId: runtime.devId,
20
20
  });
21
21
  const list = await getBoundLockListApi(runtime.devId);
22
- const items = (Array.isArray(list) ? list : []).map((lock) => ({
22
+ const rawItems = Array.isArray(list) ? list : [];
23
+ const securityEnabled = isFitSecurityChannelEnabledForRuntime(runtime);
24
+ const securityUpdatedIds = securityEnabled && rawItems.some((lock) => lock.isSupportScy)
25
+ ? await getSecurityUpdatedLockIds(runtime.devId)
26
+ : [];
27
+ const items = rawItems.map((lock) => ({
23
28
  ...lock,
24
29
  securityVersionMismatch: isSecurityVersionMismatch(runtime, lock),
30
+ securityNeedUpdate: securityEnabled &&
31
+ !!lock.isSupportScy &&
32
+ !securityUpdatedIds.includes(lock.devId),
25
33
  }));
26
34
  sdkLog("info", "fit", "getBoundLockListSuccess", {
27
35
  devId: runtime.devId,
@@ -29,6 +37,8 @@ export async function getBoundLockListForRuntime(runtime) {
29
37
  data: {
30
38
  count: items.length,
31
39
  securityMismatchCount: items.filter((item) => item.securityVersionMismatch).length,
40
+ securityNeedUpdateCount: items.filter((item) => item.securityNeedUpdate)
41
+ .length,
32
42
  },
33
43
  });
34
44
  return items;
@@ -1,6 +1,6 @@
1
1
  import dpUtils from "@ray-js/tuya-dp-transform";
2
2
  import { addFitOpenMode } from "../../../api/fit";
3
- import { sleep } from "../../../utils";
3
+ import { formatCloudUnlockId, sleep } from "../../../utils";
4
4
  import { DPCHANGE } from "../../../utils/constant";
5
5
  import { getError } from "../../../utils/errors";
6
6
  import emitter from "../../../utils/event";
@@ -195,7 +195,7 @@ const handleAddReport = async (dps) => {
195
195
  await addFitOpenMode({
196
196
  devIdList: [addUnlockMethodData.lockDevId],
197
197
  fittingId: rt.devId,
198
- unlockId: `${addUnlockMethodData.unlockDpId}-${result.unlockId}`,
198
+ unlockId: formatCloudUnlockId(addUnlockMethodData.unlockDpId, result.unlockId),
199
199
  unlockName: "",
200
200
  userId: addUnlockMethodData.userId,
201
201
  hijack: false,
@@ -1,6 +1,6 @@
1
1
  import dpUtils from "@ray-js/tuya-dp-transform";
2
2
  import { addFitOpenMode } from "../../../api/fit";
3
- import { sleep } from "../../../utils";
3
+ import { formatCloudUnlockId, sleep } from "../../../utils";
4
4
  import { getError, handleCloudError } from "../../../utils/errors";
5
5
  import { publishDpsForRuntime } from "../../dp/publish-dps";
6
6
  import { getFitUnlockMethodBase } from "./internal/base";
@@ -75,7 +75,7 @@ export async function fitAddPasswordForRuntime(rt, params) {
75
75
  await addFitOpenMode({
76
76
  devIdList: [lockDevId],
77
77
  fittingId,
78
- unlockId: `${dpId}-${res.unlockId}`,
78
+ unlockId: formatCloudUnlockId(dpId, res.unlockId),
79
79
  unlockName: params.name,
80
80
  userId: params.userId,
81
81
  hijack: !!params.hijack,
@@ -1,2 +1,3 @@
1
1
  import type { LockDeviceRuntime } from "../../runtime/LockDeviceRuntime";
2
2
  export declare const isFitSecurityChannelEnabledForRuntime: (rt: LockDeviceRuntime) => boolean;
3
+ export declare const getSecurityUpdatedLockIds: (fittingId: string) => Promise<string[]>;
@@ -1,3 +1,5 @@
1
+ import { getDeviceProperties } from "../../api/setting";
2
+ import { sdkLog } from "../../utils/sdk-log";
1
3
  export const isFitSecurityChannelEnabledForRuntime = (rt) => {
2
4
  const raw = rt.config.devInfo?.meta?.["btScyChannel"];
3
5
  if (!raw) {
@@ -13,3 +15,29 @@ export const isFitSecurityChannelEnabledForRuntime = (rt) => {
13
15
  return false;
14
16
  }
15
17
  };
18
+ export const getSecurityUpdatedLockIds = async (fittingId) => {
19
+ try {
20
+ const props = await getDeviceProperties(fittingId, ["SECURITY_UPDATED"]);
21
+ const raw = props?.["SECURITY_UPDATED"];
22
+ if (typeof raw === "string") {
23
+ try {
24
+ return JSON.parse(raw).value || [];
25
+ }
26
+ catch {
27
+ return [];
28
+ }
29
+ }
30
+ if (raw && typeof raw === "object") {
31
+ return (raw.value ||
32
+ (Array.isArray(raw) ? raw : []));
33
+ }
34
+ return [];
35
+ }
36
+ catch (e) {
37
+ sdkLog("warn", "fit", "getSecurityUpdatedError", {
38
+ devId: fittingId,
39
+ error: e,
40
+ });
41
+ return [];
42
+ }
43
+ };
@@ -2,6 +2,14 @@ export type FitBindOperation = "bind" | "unbind";
2
2
  export declare const FIT_BIND_TIMEOUT_MS = 12000;
3
3
  export declare const FIT_OPERATION_CODE: Record<FitBindOperation, "00" | "01">;
4
4
  export declare const normalizeHex: (value: string) => string;
5
+ export declare function markPrecautionaryUnbindPublished(fittingId: string, fit: {
6
+ fittingUnlockId: string;
7
+ randomStr: string;
8
+ devUnlockId: string;
9
+ }): void;
10
+ export declare function clearPrecautionaryUnbindSuppression(fittingId: string): void;
11
+ export declare function shouldSuppressFitSyncReportFrame(fittingId: string, data: string): boolean;
12
+ export declare function normalizeDevUuidHex(devUuid: string): string;
5
13
  export declare function buildTransparentData(operation: FitBindOperation, fit: {
6
14
  fittingUnlockId: string;
7
15
  randomStr: string;
@@ -7,8 +7,43 @@ export const FIT_OPERATION_CODE = {
7
7
  unbind: "01",
8
8
  };
9
9
  export const normalizeHex = (value) => value.replace(/\s/g, "");
10
+ const PRECAUTIONARY_UNBIND_SUPPRESS_MS = 60_000;
11
+ const pendingPrecautionaryUnbindAcks = new Map();
12
+ const buildUnbindAckPrefix = (fit) => `01${fit.fittingUnlockId}${fit.randomStr}${fit.devUnlockId}`.toLowerCase();
13
+ export function markPrecautionaryUnbindPublished(fittingId, fit) {
14
+ pendingPrecautionaryUnbindAcks.set(fittingId, {
15
+ prefix: buildUnbindAckPrefix(fit),
16
+ expiresAt: Date.now() + PRECAUTIONARY_UNBIND_SUPPRESS_MS,
17
+ });
18
+ }
19
+ export function clearPrecautionaryUnbindSuppression(fittingId) {
20
+ pendingPrecautionaryUnbindAcks.delete(fittingId);
21
+ }
22
+ export function shouldSuppressFitSyncReportFrame(fittingId, data) {
23
+ const entry = pendingPrecautionaryUnbindAcks.get(fittingId);
24
+ if (!entry) {
25
+ return false;
26
+ }
27
+ if (Date.now() > entry.expiresAt) {
28
+ pendingPrecautionaryUnbindAcks.delete(fittingId);
29
+ return false;
30
+ }
31
+ return normalizeHex(data).toLowerCase().startsWith(entry.prefix);
32
+ }
33
+ export function normalizeDevUuidHex(devUuid) {
34
+ if (devUuid.length >= 32 &&
35
+ devUuid.length % 2 === 0 &&
36
+ /^[0-9a-fA-F]+$/.test(devUuid)) {
37
+ return devUuid;
38
+ }
39
+ let hex = "";
40
+ for (let i = 0; i < devUuid.length; i += 1) {
41
+ hex += devUuid.charCodeAt(i).toString(16);
42
+ }
43
+ return hex;
44
+ }
10
45
  export function buildTransparentData(operation, fit, loginKey) {
11
- return `${FIT_OPERATION_CODE[operation]}${fit.fittingUnlockId}${fit.randomStr}${fit.devUnlockId}${fit.devUuid}${loginKey}`;
46
+ return `${FIT_OPERATION_CODE[operation]}${fit.fittingUnlockId}${fit.randomStr}${fit.devUnlockId}${normalizeDevUuidHex(fit.devUuid)}${loginKey}`;
12
47
  }
13
48
  export function buildLockCheckCodeTemplate(operation, fit) {
14
49
  return `ffff${fit.devUnlockId}${FIT_OPERATION_CODE[operation]}${fit.fittingUnlockId}${fit.randomStr}`;
@@ -106,8 +141,12 @@ export async function publishFitTransparentFlow(fittingId, keyDeviceId, operatio
106
141
  expectedOperation: FIT_OPERATION_CODE[operation],
107
142
  });
108
143
  if (operation === "bind") {
144
+ markPrecautionaryUnbindPublished(fittingId, fit);
109
145
  await publishBLETransparentData(fittingId, buildTransparentData("unbind", fit, loginKey));
110
146
  }
147
+ else {
148
+ clearPrecautionaryUnbindSuppression(fittingId);
149
+ }
111
150
  await publishBLETransparentData(fittingId, buildTransparentData(operation, fit, loginKey));
112
151
  await waitResult;
113
152
  sdkLog("info", "fit", "transparentFlowSuccess", {
@@ -2,6 +2,7 @@ import { syncFitToLockRise, syncFitReport as syncFitReportApi, syncOpenModeRise
2
2
  import { unlockMethodDpCodes } from "../utils";
3
3
  import { offBLETransparentDataReport, onBLETransparentDataReport, publishBLETransparentData, subscribeBLETransparentDataReport, } from "../../platform/device";
4
4
  import { sdkLog } from "../../utils/sdk-log";
5
+ import { shouldSuppressFitSyncReportFrame } from "./shared";
5
6
  export async function syncFitBindingsForRuntime(runtime) {
6
7
  const startedAt = Date.now();
7
8
  const fittingId = runtime.devId;
@@ -57,6 +58,12 @@ export function startFitSyncReportForRuntime(runtime) {
57
58
  if (params?.deviceId !== devId || !params?.data) {
58
59
  return;
59
60
  }
61
+ if (shouldSuppressFitSyncReportFrame(devId, params.data)) {
62
+ sdkLog("info", "fit", "syncReportSuppressPrecautionaryUnbindAck", {
63
+ devId,
64
+ });
65
+ return;
66
+ }
60
67
  syncFitReportApi(devId, params.data).catch((e) => {
61
68
  sdkLog("warn", "fit", "syncReportAutoError", {
62
69
  devId,
@@ -1,7 +1,7 @@
1
1
  import dpUtils from "@ray-js/tuya-dp-transform";
2
2
  import { createUnlockMethod } from "../../../api/lock";
3
3
  import { parallelOnlyForRuntime } from "../../utils";
4
- import { sleep } from "../../../utils";
4
+ import { formatCloudUnlockId, sleep } from "../../../utils";
5
5
  import { getError } from "../../../utils/errors";
6
6
  import { publishDpsForRuntime } from "../../dp/publish-dps";
7
7
  import emitter from "../../../utils/event";
@@ -207,7 +207,7 @@ const handleAddReport = async (dps) => {
207
207
  const res = await createUnlockMethod({
208
208
  devId: cfg.devInfo.devId,
209
209
  userId: addUnlockMethodData.userId,
210
- unlockId: `${addUnlockMethodData.unlockDpId}-${result.unlockId}`,
210
+ unlockId: formatCloudUnlockId(addUnlockMethodData.unlockDpId, result.unlockId),
211
211
  unlockName: "",
212
212
  unlockAttr: 0,
213
213
  });
@@ -1,7 +1,7 @@
1
1
  import dpUtils from "@ray-js/tuya-dp-transform";
2
2
  import { createUnlockMethod } from "../../../api/lock";
3
3
  import { encrypt } from "../../../platform/device";
4
- import { sleep } from "../../../utils";
4
+ import { formatCloudUnlockId, sleep } from "../../../utils";
5
5
  import { getError, handleCloudError } from "../../../utils/errors";
6
6
  import { publishDpsForRuntime } from "../../dp/publish-dps";
7
7
  import { getUnlockMethodBase } from "./internal/base";
@@ -76,7 +76,7 @@ export async function addPasswordForRuntime(rt, params) {
76
76
  const result = await createUnlockMethod({
77
77
  devId: devId,
78
78
  userId: params.userId,
79
- unlockId: `${dpId}-${res.unlockId}`,
79
+ unlockId: formatCloudUnlockId(dpId, res.unlockId),
80
80
  unlockName: params.name,
81
81
  unlockDetail: aesPwd,
82
82
  ...specialInfo,
@@ -121,6 +121,7 @@ export declare function getCapabilities(capability: number): ({
121
121
  readonly short: "thread";
122
122
  })[];
123
123
  export declare const formatWeek: (value: number[]) => number;
124
+ export declare const formatCloudUnlockId: (dpId: number | string, sn: number) => string;
124
125
  export declare const parseWeek: (value: number) => Week;
125
126
  export declare const getPermanentSetting: () => {
126
127
  startTime: number;
@@ -56,6 +56,7 @@ export const formatWeek = (value) => {
56
56
  const temp = value.slice(0, 7);
57
57
  return parseInt(temp.reverse().join(""), 2);
58
58
  };
59
+ export const formatCloudUnlockId = (dpId, sn) => `${dpId}-${sn.toString(16).padStart(2, "0")}`;
59
60
  export const parseWeek = (value) => {
60
61
  return Number(value)
61
62
  .toString(2)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ray-js/lock-sdk",
3
- "version": "1.2.0-beta.25",
3
+ "version": "1.2.0-beta.26",
4
4
  "files": [
5
5
  "lib",
6
6
  "LICENSE.md"