Prerequisites

Create a panel

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.

Learning objectives

Prerequisites

For more information, see Panel MiniApp > Set up environment.

Product name: Bluetooth lock

Requirement prototype

Description of DPs

The current smart lock template must include the following DPs to implement local unlocking logic.

check_code_set,
ble_unlock_check,

Configure Bluetooth unlocking with verification code

Parameter

Value

dpid

70

code

check_code_set

type

Raw

mode

Send and report (read-write)

Bluetooth unlocking with verification code

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.

  1. In the left-side navigation pane, choose Product > Development > Create.
  2. In the Standard Category tab, choose Smart Locks > Residential Lock Pro.
  3. Select a smart mode and solution, and complete product information. For example, specify Product Name as PanelLock and Protocol as Bluetooth.
  4. Click Create.
  5. After your product is created, the Add Standard Function dialog box appears. You can keep the default selections.

Create panel miniapp on Smart MiniApp Developer Platform

Register and log in to the Smart MiniApp Developer Platform.

For more information, see Create panel miniapp.

Create a project based on a template

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 dependencies and configuration

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'
}

Configure the lock status and color

// 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 components and manage states

// 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]);

Event handler and logic functions

  // 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);
  };

Render the remote unlocking component

  // 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.

src/pages/log/index.tsx displays unlocking, locking, alert, and operation records in the panel, which can be filtered by time and type.

Import tool functions and configuration

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';

Define components and manage states

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
  }), []);

Log request logic

  // 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();
  };

Logic of the type selection modal dialog

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>
    ),
  });

Page rendering logic

  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 dependencies and utility functions

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;

API class related to Bluetooth temporary passwords

/** 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 DP data

  /**
   * 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') });
          }
        }
      });
    });
  };

Generate the DP data

  /**
   * 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 dependencies and utility functions

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';

Define components and manage states

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]);

Logic of enumeration type modal dialog

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>
    ),
  });

Page rendering logic

  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;