In Codelab, you can utilize the panel miniapp to develop a Bluetooth lock panel. Using the DP protocol and APIs, you can implement Bluetooth (through the gateway) locking and unlocking, member management, temporary password setting, locking and unlocking log, alert log, regular door lock, and other settings.
For more information, see Panel MiniApp > Set up environment.
Product name: Bluetooth lock
The current smart lock template must include the following DPs to implement local unlocking logic.
check_code_set,
ble_unlock_check,
Parameter | Value |
dpid | 70 |
code | check_code_set |
type | Raw |
mode | Send and report (read-write) |
Parameter | Value |
dpid | 71 |
code | ble_unlock_check |
type | Raw |
mode | Send and report (read-write) |
If you want to implement Bluetooth locking, select the manual_lock
DP. For remote unlocking, select the remote_no_dp_key
and ble_unlock_check
DPs. For more information, see Data Point Reference.
A product defines the data points (DPs) of the associated panel and device. Before you develop a panel, you must create a product, define the required DPs, and then implement these DPs on the panel.
Register and log in to the Tuya Developer Platform and create a product.
Register and log in to the Smart MiniApp Developer Platform.
For more information, see Create panel miniapp.
Open Tuya MiniApp IDE and create a panel miniapp project based on the smart lock template. For more information, see Initialize project.
A panel miniapp template has already been initialized through the previous steps. The following section shows the project directories.
├── src
│ ├── api // Cloud APIs
│ │ ├── base.ts // Base class APIs
│ │ ├── bleApi.ts // APIs related to Bluetooth
│ │ ├── getCachedLaunchOptions.ts // Get startup parameters
│ │ ├── getCachedSystemInfo.ts // Get device information
│ │ ├── homeApi.ts // APIs related to the homepage
│ │ ├── interface.ts // API definition
│ │ ├── logsApi.ts // APIs related to logs
│ │ ├── setting.ts // APIs related to settings
│ │ └── index.ts
│ ├── components
│ │ ├── DateRangePicker // Date range picker
│ │ ├── DateTimePicker // Date and time picker
│ │ ├── DefendTime // Arming
│ │ ├── Empty // Empty state
│ │ ├── FamilyIcon // Home icon
│ │ ├── LatestLog // The most recent log
│ │ ├── OfflinePwdFooter // Offline password footer
│ │ ├── PanelList // List
│ │ ├── PasswordIcon // Password icon
│ │ ├── PasswordNameInput // Password name input
│ │ ├── PasswordPanel // Password
│ │ ├── RandomPassword // Random password
│ │ ├── RecordItem // Record
│ │ ├── RemoteOpenDoor // Remote unlocking
│ │ ├── RotateImage // Image rotation
│ │ ├── StatusBar // Lock status bar
│ │ ├── connect.tsx
│ │ └── index.ts
│ ├── config // Common configuration
│ ├── constant // Common constants
│ ├── i18n // Multilingual settings
│ ├── hooks // Common hooks
│ ├── models // Redux
│ │ ├── combine.ts // CombineReducers
│ │ ├── configureStore.ts
│ │ ├── index.ts
│ │ ├── store.ts
│ │ └── modules
│ │ ├── common.ts // Common actions and reducers
│ ├── pages
│ │ ├── home // Homepage
│ │ ├── log // Log
│ │ ├── family // User management
│ │ ├── family-user-detail // User details
│ │ ├── family-add-unlock // Add unlocking methods
│ │ ├── family-edit-unlock // Edit unlocking methods
│ │ ├── setting // Settings
│ │ ├── temp // Temporary password
│ │ ├── temp-record // List of temporary passwords
│ │ ├── temp-invalid-record // List of invalid temporary passwords
│ │ ├── temp-emptyCode // Clear code
│ │ ├── temp-update // Update custom passwords
│ ├── res // Local pictures
│ ├── utils // Common utility methods
│ ├── app.config.ts
│ ├── app.tsx
│ ├── composeLayout.tsx // Handle and listen for the adding, unbinding, and DP changes of sub-devices
│ ├── global.config.ts
│ ├── routes.config.ts //Configure routing
The src/pages/home/index.tsx
file looks like this:
import { hooks } from "@ray-js/panel-sdk";
import { connectBLEDevice } from "@/utils/ble";
const { useDeviceOnline } = hooks;
const Home = () => {
const { deviceOnline } = useDeviceOnline();
useEffect(() => {
// When the device goes offline, proactively connect to a Bluetooth device
if (!deviceOnline) { connectBLEDevice();
}
}, [deviceOnline]);
return (
<View>
<Text>Home</Text>
</View>
);
};
export default Home;
The src/utils/ble.ts
file looks like this:
import { showLoading, hideLoading, showToast } from "@ray-js/ray";
import Strings from "@/i18n";
import { getCachedSystemInfo } from "@/api/getCachedSystemInfo";
import { getCachedLaunchOptions } from "@/api/getCachedLaunchOptions";
const devId = getCachedLaunchOptions()?.query?.deviceId;
const systemInfo = getCachedSystemInfo();
/**
* Connect to a Bluetooth device
* @param {number} [timeout=10000] - The timeout value
*/
const connectBLEDevice = (timeout = 10000) => {
// Indicate whether Bluetooth is enabled
ty.device.bluetoothIsPowerOn({
success: (res) => {
if (!res) {
// Enable Bluetooth settings on Android devices
if (systemInfo.platform === "android") {
ty.openSystemBluetoothSetting({
success: () => {
showLoading({ title: Strings.getLang("connectBLEDevice") });
startConnectBLEDevice(timeout);
},
fail: (res) => {
console.log("openBluetoothAdapter fail", res);
},
});
} else {
showToast({ title: Strings.getLang("openPhone"), icon: "error" });
}
} else {
showLoading({ title: Strings.getLang("connectBLEDevice") });
startConnectBLEDevice(timeout);
}
},
fail: (res) => {
showToast({ title: Strings.getLang("connectFail"), icon: "error" });
hideLoading();
},
});
};
/**
* Start to connect to a Bluetooth device
* @param {number} [timeout=10000] - The timeout value
*/
const startConnectBLEDevice = (timeout = 10000) => {
console.log("Proactively connect to a Bluetooth device");
ty.device.connectBluetoothDevice({
devId: devId,
timeoutMillis: timeout,
souceType: 1,
connectType: 1,
success: () => {
showToast({ title: Strings.getLang("openSuccess"), icon: "success" });
},
fail: (res) => {
showToast({ title: Strings.getLang("connectFail"), icon: "error" });
},
});
};
export { connectBLEDevice };
In src/components/RemoteOpenDoor/index.tsx
, get the lock_motor_state
DP state in real time. When users tap and hold a specific UI button, the door opens and closes based on the DP state.
The comments in the following code snippet explain each part in detail, helping you or other readers better understand the structure and features of the code snippet.
import { useMemo, useState, useRef, useEffect } from 'react';
// Import UI components and utility functions
import { Text, View, Image, router, showToast, hideToast, onDpDataChange, offDpDataChange, showModal } from '@ray-js/ray';
import { utils } from '@ray-js/panel-sdk';
import { useStore } from 'react-redux';
import { RotateImage } from '@/components';
// Import resource files and internationalized strings
import Res from '@/res';
import Strings from '@/i18n';
// Import tool functions and API methods
import { formatDpChange, putDpData, connectBLEDevice } from '@/utils';
import { dpCodes, globalConfig } from '@/config';
import { remoteOpen as remoteOpenApi } from '@/api/homeApi';
// Import custom hooks
import { usePanelStore, useDeviceOnline, useDevInfo, useDpPermission } from '@/hooks';
// Import the style file
import styles from './index.module.less';
// Get the device property code from the configuration file
const { remoteNoDpKey, remoteUnlockCheck, manualLock } = dpCodes;
// Define door status enumeration values
enum DoorStatus {
open = 'open',
close = 'close',
offline = 'offline'
}
// Define the UI display information in different door status
const statusInfo = {
open: { bg: Res.showOpenImage, iconBg: Res.openImage, text: Strings.getLang('close'), color: '#F67352' },
close: { bg: Res.showImage, iconBg: Res.lockImage, text: Strings.getLang('clickOpen'), color: '#5EAAFF' },
offline: { bg: Res.outLineImage, iconBg: '', text: Strings.getLang('off'), color: '#A2A2A2' }
};
// Define the RemoteOpenDoor component
const RemoteOpenDoor = () => {
// Use Redux store
const store = useStore();
// Use custom hooks to get device states and properties
const { dpState, lockData, deviceProps, user } = usePanelStore();
const deviceOnline = useDeviceOnline();
const devInfo = useDevInfo();
const { dpRemotePdSetkeyCheck, dpUnlockRecordCheck } = useDpPermission();
const { remoteSwitch, bleOnlineState } = deviceProps;
const { lock_motor_state } = dpState;
const { isGW = false } = devInfo || {};
// Define the internal state of the component
const [loading, setLoading] = useState(false);
const timer = useRef(null);
const loadRef = useRef(loading);
const themeColor = globalConfig.getConfig('themeColor') as string;
// Use useMemo to calculate the door state
const status = useMemo(() => deviceOnline ? (lock_motor_state ? DoorStatus.open : DoorStatus.close) : DoorStatus.offline, [deviceOnline, lock_motor_state]);
// Get the display text based on the door state
const text = useMemo(() => statusInfo[status].text, [status, loading, deviceOnline, lock_motor_state]);
// Listen for DP changes
useEffect(() => {
onDpDataChange(dpDataChangeHandle);
return () => offDpDataChange(dpDataChangeHandle);
}, []);
// Synchronize loading status to Ref
useEffect(() => {
loadRef.current = loading;
}, [loading]);
// DP change handler
const dpDataChangeHandle = data => {
const { getState } = store;
const { devInfo: _devInfo } = getState();
const _dpState = formatDpChange(_devInfo.arrSchema, data.dps);
if (Object.keys(_dpState).length > 10) return;
// Logic for handling DP changes
};
// Remote unlocking function
const remoteOpen = async () => {
if (loading) return;
if (!deviceOnline) {
showModal({
title: Strings.getLang('off'), content: Strings.getLang('timeout'), confirmText: Strings.getLang('confirm'), confirmColor: themeColor, showCancel: false,
success: res => { if (res.confirm) { connectBLEDevice(); } }
});
return;
}
setLoading(true);
// Execute remote unlocking logic
};
// Set the timeout timer
const setTimer = () => {
timer.current = setTimeout(() => {
if (loadRef.current) {
showToast({ title: Strings.getLang('timeout'), icon: 'none' });
} else {
hideToast();
}
setLoading(false);
}, 15000);
};
// Clear the timer
const clearTimer = () => {
timer.current && clearTimeout(timer.current);
};
// Component rendering logic
return (
<View className={styles.container}>
{status !== DoorStatus.offline && (
<RotateImage active={loading} rotateImage={statusInfo[status].bg} onLongClick={() => remoteOpen()}>
<View className={styles.centerView}>
{!!statusInfo[status].iconBg && <Image src={statusInfo[status].iconBg} className={styles.centerImage} />}
<Text className={styles.lockText} style={{ color: statusInfo[status].color}}>{text}</Text>
</View>
</RotateImage>
)}
{status === DoorStatus.offline && (
<View className={styles.outLineWrap}>
<Image src={statusInfo[status].bg} className={styles.outLineBg} />
<Text className={styles.outLineText} style={{ color: statusInfo[status].color }}>{text}</Text>
</View>
)}
</View>
);
};
export default RemoteOpenDoor;
After the sample code is imported, the homepage displays a circular button that can be tapped and held. You can go to the right-side Control Panel on the IDE, find Lock Status (lock_motor_state
), and then select true or false. This allows you to simulate DP sending and reporting and verify the product features on the virtual panel.
Add, edit, and delete unlocking methods in src/pages/family-user-detail/index.tsx
.
const addUnlock = (dpCode) => {
// Ordinary users do not have permission to add unlocking methods
if (ordinaryUser) {
showToast({ title: Strings.getLang('hasNoPermissionAdd'), icon: 'none' });
return;
}
// When the device goes offline, proactively connect to Bluetooth
if (!deviceOnline) {
connectBLEDevice();
return;
}
// Assemble DP data
const type = getModeType[dpCode];
const phase = '00'; // Start enrollment
const adminType = getUserType[userType];
const _userId = utils.numToHexString(lockUserId || userId); // User ID
const hardwareNumber = 'ff'; // hardware serial number
const time = timeScheduleInfo.userTimeSet; // Valid period
const dpData = `${type}${phase}${adminType}${_userId}${hardwareNumber}${time}000000`;
putDpDatas.current = dpData;
// Enter the page to enroll the unlocking method with password
if (dpCode === unlockPassword) {
router.push(`/family-add-unlock?dpCode=${dpCode}&userId=${userId}&putDpDatas=${dpData}`);
return;
}
// Send DP data to create unlocking methods
showToast({ title: Strings.getLang('connecting'), icon: 'none' });
putDpData(unlockMethodCreate, dpData);
};
src/pages/family-user-detail/component/RecordItem/index.tsx
.import { FC, useState } from 'react';
import { View, useQuery, Text, Input, showLoading, hideLoading, router, showToast } from '@ray-js/ray';
import { useRequest } from 'ahooks';
import { usePageEvent } from 'ray';
import { globalConfig } from '@/config';
import { Api } from '@/api';
const Index: FC = () => {
const [detailInfo, setDetailInfo] = useState({ unlockName: '' });
const [name, setName] = useState('');
const query = useQuery();
// Trigger a data request when the page is displayed
usePageEvent('onShow', () => run());
// Get unlocking method details
const { run } = useRequest(() => Api.apis.fetchUnlockDetail(query.opModeId), {
onBefore: () => showLoading({ title: '', mask: true }),
onSuccess: (data) => {
setDetailInfo(data);
hideLoading();
},
onError: hideLoading,
manual: true,
ready: query && query.opModeId,
});
// Update unlocking method details
const { run: update } = useRequest(Api.apis.updateUnlock, {
onBefore: () => showLoading({ title: Strings.getLang('changing'), mask: true }),
onSuccess: () => {
hideLoading();
showToast({ title: Strings.getLang('Password_form_modifySuccess'), icon: 'success' });
router.back();
},
onError: () => {
hideLoading();
showToast({ title: Strings.getLang('Password_form_modifyError'), icon: 'fail' });
},
manual: true,
});
// Delete unlocking methods
const { run: deleteUnlock } = useRequest(Api.apis.deleteUnlock, {
onBefore: () => showLoading({ title: Strings.getLang('deleting'), mask: true }),
onSuccess: () => {
hideLoading();
showToast({ title: Strings.getLang('Password_delete_success'), icon: 'success' });
router.back();
},
onError: () => {
hideLoading();
showToast({ title: Strings.getLang('Password_delete_failure'), icon: 'fail' });
},
manual: true,
});
};
export default Index;
src/pages/log/index.tsx
displays unlocking, locking, alert, and operation records in the panel, which can be filtered by time and type.
import React, { FC, useState, useReducer } from 'react';
import { useCreation, useRequest } from 'ahooks';
import { View, Text, ScrollView, showLoading, hideLoading } from '@ray-js/ray';
import List from '@ray-js/components-ty-cell';
import Icon from '@ray-js/components/lib/Icon';
import ActionSheet from '@ray-js/components-ty-actionsheet';
import _uniq from 'lodash/uniq';
import { globalConfig } from '@/config';
import Empty from '@/components/Empty';
import { getLogs } from '@/api/logsApi';
import { formatLog, logCategoryList, timeList, getSelectTime } from '@/utils';
import Strings from '@/i18n';
import styles from './index.module.less';
const Index: FC = () => {
// State management
const [category, setCategory] = useState(''); // Currently selected log type
const [tempCategory, setTempCategory] = useState(''); // Temporarily selected log type
const [time, setTime] = useState('time'); // Currently selected time range
const [tempTime, setTempTime] = useState('time'); // Temporarily selected time range
const [, forceUpdate] = useReducer(x => x + 1, 0); // Forced update of the component
const themeColor = globalConfig.getConfig('themeColor') as string; // Theme color
// Reference management
const Ref = useCreation(() => ({
lastRowKey: '', // The key of the last row requested last time
hasMore: false, // Indicates whether there is more data
logsData: [] // The log data
}), []);
// Request logic for getting log data
const { run } = useRequest(() => getLogs({
logCategories: category,
startTime: getSelectTime(time)[1],
endTime: getSelectTime(time)[0],
lastRowKey: Ref.lastRowKey,
}), {
onBefore: () => showLoading({ title: '', mask: true }), // Show loading before request
onSuccess(data) {
Ref.lastRowKey = data.lastRowKey;
Ref.hasMore = data.hasMore;
Ref.logsData = _uniq([...Ref.logsData, ...data.records]); // Merge and remove duplicate log data
forceUpdate();
hideLoading(); // Hide loading after the request is successful
},
onError: hideLoading, // Hide loading after the request fails
refreshDeps: [category, time], // Re-request when dependencies change
});
// Trigger loading more data when the page is scrolled to the bottom
const onScrollToLower = () => {
if (Ref.hasMore) run();
};
The logic is similar to the time selection modal dialog, so there is no need to elaborate on it.
// Logic of the type selection modal dialog
const { modal: categoryModal, setVisible: setShowCategory, visible: showCategory } = ActionSheet.useActionSheet({
header: Strings.getLang('pleaseSelect'),
onOk: () => {
Ref.lastRowKey = '';
Ref.logsData = [];
setCategory(tempCategory);
setShowCategory(false);
},
onCancel: () => setShowCategory(false),
okText: Strings.getLang('confirm'),
cancelText: Strings.getLang('cancel'),
onClickOverlay: () => setShowCategory(false),
content: () => (
<ScrollView scrollY className={styles.modalWarp}>
<List.Row
dataSource={logCategoryList}
rowKey="key"
renderItem={item => (
<List.Item
className={styles.listItem}
title={item.title}
onClick={() => setTempCategory(item.key)}
content={item.key === tempCategory ? <Icon type="icon-checkmark" size={24} color={themeColor} /> : null}
/>
)}
splitStyle={{ width: '100%', left: 0 }}
/>
</ScrollView>
),
});
return (
<>
<View className={styles.container}>
<View className={styles.tabItem} onClick={() => setShowCategory(!showCategory)}>
<Text>{logCategoryList.find(item => item.key === category)?.title || Strings.getLang('allCategories')}</Text>
<Icon type="icon-down" size={24} color="#999" />
</View>
<View className={styles.tabItem} onClick={() => setShowTime(!showTime)}>
<Text>{timeList.find(item => item.key === time)?.title || Strings.getLang('allTimes')}</Text>
<Icon type="icon-down" size={24} color="#999" />
</View>
</View>
{categoryModal}
{timeModal}
<ScrollView scrollY className={styles.warp} onScrollToLower={onScrollToLower}>
{Ref.logsData.length > 0 ? (
Ref.logsData.map(item => {
const { formatTime, formatUserName, openText } = formatLog(item);
return (
<View key={item.logId} className={styles.item}>
<Text>{`${formatTime} ${formatUserName ? `【${formatUserName}】` : ''} ${openText}`}</Text>
</View>
);
})
) : (
<Empty />
)}
</ScrollView>
</>
);
};
export default Index;
The overall code regarding temporary passwords is quite large, so this section only shows the most complex part of the logic. src/api/bleApi.ts
shows how to add, delete, and modify custom passwords in the temporary password.
import { utils } from '@ray-js/panel-sdk';
import {
showToast,
onDpDataChange,
offDpDataChange,
deleteTemporaryPassword,
createTemporaryPassword,
updateTempPassword,
} from '@ray-js/ray';
import { padStart } from 'lodash';
import Strings from '@/i18n';
import { store } from '@/redux';
import { putDpData } from '@/utils/sendDp';
import { connectBLEDevice } from '@/utils/ble';
import { ApiCreatorBase } from './base';
import { getCachedLaunchOptions } from './getCachedLaunchOptions';
import {
ICreatePasswordParams,
IDeleteParams,
IUpdateParams,
IScheduleType,
ICreatePasswordResult,
} from './interface';
const { numToHexString } = utils;
/** APIs for Bluetooth locks */
export default class BlueToothAPi extends ApiCreatorBase {
/**
* Create a temporary password
* @param params Parameters for creating a password
* @returns Specifies whether the password was created successfully
*/
createPwd<T = ICreatePasswordParams, R = ICreatePasswordResult>(params: T): Promise<R> {
return this.listenCreateDp(params)
.then((res: R) => {
offDpDataChange(() => {});
return res;
});
}
/**
* Update the specified temporary password
* @param params Parameters for updating the password
* @returns Specifies whether the password was updated successfully
*/
updatePwd(params: IUpdateParams): Promise<boolean> {
return this.listenUpdateDp(params)
.then(() => {
offDpDataChange(() => {});
return true;
});
}
/**
* Delete the specified temporary password
* @param params Parameters for deleting the password
* @returns Specifies whether the password was deleted successfully
*/
deletePwdApi(params: Omit<IDeleteParams, 'deviceOnline'>): Promise<boolean> {
const devId = getCachedLaunchOptions()?.query?.deviceId;
return deleteTemporaryPassword({
devId,
unlockBindingId: params.id,
});
}
/**
* Before deleting the temporary password, determine whether the device is online first
* @param params Parameters for deleting the password
* @returns Specifies whether the password was deleted successfully
*/
deletePwd({ deviceOnline, ...restParams }: IDeleteParams): Promise<boolean> {
if (!deviceOnline) {
connectBLEDevice();
return Promise.reject({ message: Strings.getLang('off') });
}
if (restParams.pwdType === 0) {
putDpData('temporary_password_delete', numToHexString(restParams.sn as number, 2));
return this.listenDeleteDp(restParams.id as number).then(() => true);
}
return this.checkAndDeletePwd(restParams);
}
/**
* Listen for changes in the DP of deleting the temporary password
* @param deleteId The ID of the password to be deleted
* @returns A promise that specifies whether the password was deleted successfully
*/
listenDeleteDp = (deleteId: number) => {
return new Promise((resolve, reject) => {
onDpDataChange(data => {
const { devInfo } = store.getState();
const { schema } = devInfo;
const dpId = schema['temporary_password_delete'].id;
const temPwdDelete = data.dps[dpId] as string;
if (temPwdDelete) {
const status = temPwdDelete.slice(2, 4); // Return the operation status
if (status === '00') {
this.deletePwdApi({ id: deleteId })
.then(resolve)
.catch(reject);
showToast({ title: Strings.getLang('Password_delete_success'), icon: 'success' });
} else {
reject();
showToast({ title: Strings.getLang('Password_delete_failure'), icon: 'error' });
}
}
});
});
};
/**
* Listen for changes in the DP of creating a temporary password
@param params Parameters for creating a password
* @returns Specifies whether the password was created successfully
*/
listenCreateDp = async <R = any, T = ICreatePasswordResult>(params: any) => {
const scheduleInfo = {
beginTime: params.effectiveTime,
endTime: params.invalidTime,
allDay: true,
effectiveTime: 0,
invalidTime: 0,
workingDay: '',
};
Object.assign(scheduleInfo, params.schedule && JSON.parse(params.schedule)[0]);
const dpData = this.getCreatePwdDpData(scheduleInfo, params.originalPassword);
putDpData('temporary_password_creat', dpData);
return new Promise((resolve, reject) => {
onDpDataChange(data => {
const { devInfo } = store.getState();
const { schema } = devInfo;
const dpId = schema['temporary_password_creat'].id;
const temPwdCreate = data.dps[dpId] as string;
if (temPwdCreate) {
const devId = getCachedLaunchOptions()?.query?.deviceId;
const unLockId = parseInt(temPwdCreate.slice(0, 2), 16);
const status = temPwdCreate.slice(2, 4); // Return the operation status
if (status === '00') {
resolve(
createTemporaryPassword({
devId,
symbolic: true, // Specifies whether to save only static data
dpTunnel: 2, // When Symbolic is false, this parameter is required. 1 means app, and 2 means cloud
availTime: 0,
sn: unLockId,
...params,
})
);
} else {
reject({ message: Strings.getLang(`Password_add_error_${status}` as any) });
}
}
});
});
};
/**
* Listen for changes in the DP of updating a temporary password
* @param params Parameters for updating the password
* @returns A promise that specifies whether the password was updated successfully
*/
listenUpdateDp = async (params: IUpdateParams) => {
const scheduleInfo = {
beginTime: params.effectiveTime,
endTime: params.invalidTime,
allDay: true,
effectiveTime: 0,
invalidTime: 0,
workingDay: '',
sn: params.sn,
};
Object.assign(scheduleInfo, params.schedule && JSON.parse(params.schedule)[0]);
const dpData = this.getUpdatePwdDpData(scheduleInfo);
putDpData('temporary_password_modify', dpData);
return new Promise((resolve, reject) => {
onDpDataChange(data => {
const { devInfo } = store.getState();
const { schema } = devInfo;
const dpId = schema['temporary_password_modify'].id;
const temPwdUpdate = data.dps[dpId] as string;
if (temPwdUpdate) {
const status = temPwdUpdate.slice(2, 4); // Return the operation status
if (status === '00') {
const updateData = { unlockBindingId: params.id, ...params };
delete updateData.id;
resolve(updateTempPassword(updateData));
} else {
reject({ message: Strings.getLang('Password_form_modifyError') });
}
}
});
});
};
/**
* Get the DP data of updating a temporary password
* @param params The valid period of the password
* @returns The DP data of updating a temporary password
*/
getUpdatePwdDpData = (params: IScheduleType): string => {
const type = '01';
const count = '00';
const pswLen = '00'; // Modify the temporary password, and set the password length to 0
const { allDay = true, beginTime, endTime, effectiveTime, invalidTime, workingDay } = params;
let time = '';
const unLockId = padStart(params.sn?.toString(16), 2, '0');
const beginTime2Hex = parseInt(String(beginTime)).toString(16);
const endTime2Hex = parseInt(String(endTime)).toString(16);
time += `${beginTime2Hex}${endTime2Hex}`;
if (allDay) {
time += '000000000000000000';
} else {
time += `02000000${padStart(parseInt(workingDay).toString(16), 2, '0')}`;
time += `${padStart(parseInt(`${effectiveTime / 60}`, 10).toString(16), 2, '0')}${padStart(
parseInt(`${+effectiveTime % 60}`, 10).toString(16),
2,
'0'
)}`;
time += `${padStart(parseInt(`${+invalidTime / 60}`, 10).toString(16), 2, '0')}${padStart(
parseInt(`${+invalidTime % 60}`, 10).toString(16),
2,
'0'
)}`;
}
return `${unLockId}${type}${time}${count}${pswLen}`;
};
/**
* Get the DP data of creating a temporary password
* @param params The valid period of the password
* @param OriginalPassword The original password
* @returns The DP data of creating a temporary password
*/
getCreatePwdDpData = (params: IScheduleType, originalPassword: string): string => {
const type = '01';
const count = '00';
const pswLen = padStart(originalPassword.length.toString(16), 2, '0');
const resPassword = [...originalPassword].map(i => padStart(Number(i).toString(16), 2, '0')).join('');
const { allDay = true, beginTime, endTime, effectiveTime, invalidTime, workingDay } = params;
let time = '';
const beginTime2Hex = parseInt(String(beginTime)).toString(16);
const endTime2Hex = parseInt(String(endTime)).toString(16);
time += `${beginTime2Hex}${endTime2Hex}`;
if (allDay) {
time += '000000000000000000';
} else {
time += `02000000${padStart(parseInt(workingDay).toString(16), 2, '0')}`;
time += `${padStart(parseInt(`${effectiveTime / 60}`, 10).toString(16), 2, '0')}${padStart(
parseInt(`${+effectiveTime % 60}`, 10).toString(16),
2,
'0'
)}`;
time += `${padStart(parseInt(`${+invalidTime / 60}`, 10).toString(16), 2, '0')}${padStart(
parseInt(`${+invalidTime % 60}`, 10).toString(16),
2,
'0'
)}`;
}
return `${type}${time}${count}${pswLen}${resPassword}`;
};
}
src/pages/setting/index.tsx
shows general settings for locks, including remote unlocking, automatic locking, double locking, voice prompts, arm away, and do-not-disturb (DND) settings.
import React, { FC, useState } from 'react';
import { useCreation } from 'ahooks';
import { DpSchema, utils } from '@ray-js/panel-sdk';
import { View, Text, ScrollView, Slider } from '@ray-js/ray';
import { globalConfig, Code } from '@/config';
import { putDpData } from '@/utils/sendDp';
import { connectBLEDevice } from '@/utils/ble';
import { saveDeviceProps } from '@/redux/actions';
import { getSettingDpSchema } from '@/utils/getSettingDpSchema';
import { usePanelStore, useDevInfo, useDeviceOnline, useUserPermission } from '@/hooks';
import { uploadSettingRecord, setDeviceProps } from '@/api/settingApi';
import Icon from '@ray-js/components/lib/Icon';
import List from '@ray-js/components-ty-cell';
import Switch from '@ray-js/components-ty-switch';
import ActionSheet from '@ray-js/components-ty-actionsheet';
import Strings from '@/i18n';
import Codes from '@/config/dpCodes';
import styles from './index.module.less';
const Index: FC = () => {
// State management
const [dpCode, setDpCode] = useState('');
const [enumValue, setEnumValue] = useState('');
const [numberValue, setNumberValue] = useState(0);
const { dpState, deviceProps } = usePanelStore();
const { isOwnerOrAdmin } = useUserPermission();
const { schema } = useDevInfo();
const deviceOnline = useDeviceOnline();
const { remoteSwitch } = deviceProps;
const themeColor = globalConfig.getConfig('themeColor') as string;
// Get the set DP schema
const settingsSchema = useCreation(() => {
return getSettingDpSchema(
schema as DpSchema[],
isOwnerOrAdmin ? Code.logTypeFormDpCodes : Code.noAdminLogTypeFormDpCodes
);
}, [schema]);
// Get the DP data source of enumeration type
const enumDpDataSource = useCreation(() => {
return settingsSchema
.find(d => d.code === dpCode)
?.property?.range?.map((rangeValue: string) => ({
label: Strings.getDpLang(dpCode, rangeValue),
value: rangeValue,
checked: rangeValue === enumValue,
}));
}, [dpCode, enumValue]);
// Get the DP data of value type
const numberDpData = useCreation(() => {
return settingsSchema.find(d => d.code === dpCode)?.property;
}, [dpCode]);
The logic is similar to the value type modal dialog, so there is no need to elaborate on it.
// Logic of enumeration type modal dialog
const {
modal: enumModal,
setVisible: setShowEnum,
visible: showEnum,
} = ActionSheet.useActionSheet({
header: Strings.getLang('pleaseSelect'),
onOk: () => {
putDpData(dpCode, enumValue);
setShowEnum(false);
uploadSettingRecord(dpCode, [enumValue]);
},
onCancel: () => setShowEnum(false),
onClickOverlay: () => setShowEnum(false),
okText: Strings.getLang('confirm'),
cancelText: Strings.getLang('cancel'),
content: () => (
<ScrollView scrollY className={styles.listWarp}>
<List.Row
className={styles.list}
dataSource={enumDpDataSource}
rowKey="label"
renderItem={item => (
<List.Item
className={styles.listItem}
title={item.label}
onClick={() => setEnumValue(item.value)}
content={item.checked ? <Icon type="icon-checkmark" size={24} color={themeColor} /> : null}
/>
)}
splitStyle={{ width: '100%', left: 0 }}
/>
</ScrollView>
),
});
return (
<ScrollView scrollY className={styles.warp}>
<List>
{/* Remote unlocking switch */}
<List.Item
title={Strings.getDpLang(Codes.remoteNoDpKey)}
content={
<Switch
style={{ marginRight: 0 }}
checkedColor={themeColor}
checked={remoteSwitch}
onChange={value => {
setDeviceProps(JSON.stringify({ UNLOCK_PHONE_REMOTE: value }));
uploadSettingRecord('remote_unlock_setting', [value.toString()]);
saveDeviceProps({ remoteSwitch: value });
}}
/>
}
/>
{/* Render DP items */}
{settingsSchema.map(item => {
let content = <></>;
const dpMode = item.mode;
const dpType = item.property?.type;
const dpUnit = item.property?.unit ?? '';
const dpScale = item.property?.scale ?? 0;
switch (dpType) {
case 'bool':
content = (
<Switch
style={{ marginRight: 0 }}
checkedColor={themeColor}
checked={!!dpState[item.code]}
onChange={value => {
if (!deviceOnline) {
connectBLEDevice();
return;
}
putDpData(item.code, value);
}}
/>
);
break;
case 'enum':
content = (
<View className={styles.valueView}>
<Text className={styles.text}>{Strings.getDpLang(item.code, dpState[item.code])}</Text>
<Icon type="icon-right" size={24} color={'rgba(0,0,0,0.2)'} />
</View>
);
break;
case 'value':
content = (
<View className={styles.valueView}>
<Text className={styles.text}>{`${utils.scaleNumber(dpScale, dpState[item.code])}${dpUnit}`}</Text>
<Icon type="icon-right" size={24} color={'rgba(0,0,0,0.2)'} />
</View>
);
break;
default:
break;
}
return (
<List.Item
key={item.code}
title={Strings.getDpLang(item.code)}
content={content}
onClick={() => {
if (dpMode === 'ro') return;
if (!['enum', 'value'].includes(dpType)) return;
if (!deviceOnline) {
connectBLEDevice();
return;
}
setDpCode(item.code);
if (dpType === 'enum') {
setEnumValue(dpState[item.code]);
setShowEnum(true);
} else if (dpType === 'value') {
setNumberValue(dpState[item.code]);
setShowValue(true);
}
}}
/>
);
})}
</List>
{enumModal}
{valueModal}
</ScrollView>
);
};
export default Index;