A control panel runs on an app and represents a product that is an abstraction of a smart device. Before you create a product, you can learn about the following correlations between control panels, products, and devices.
The following figure shows the correlations between products, control panels, and devices.
A product defines the 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.
After your product is created, the Add Standard Function dialog box appears. In the Standard Functions section, you can see a list of available standard functions. Click Select All > OK to have standard functions configured for the product.
You can then continue with Panel MiniApp development. Register and log in to the MiniApp Developer Platform.
Click Create. On the MiniApp Information page that appears, enter Universal Panel in the MiniApp Name, select Panel MiniApp as the MiniApp Type, and then click OK.
Download, install, and open the miniapp IDE tool. Then, log in to the IDE using your Tuya Developer Platform account.
To develop a panel miniapp, you rely on the Tuya Developer Platform, Smart MiniApp Developer Platform, and Smart MiniApp IDE.
After the previous steps are completed, a panel miniapp is created. Next, proceed to the actual code development tutorial.
To understand the project's directory structure, click the Open in VS Code icon in the toolbar.
If it prompts that there is no code command, you need to select Show All Commands in the Help menu of Visual Studio Code, search for code, and install it. For more information, see the official documentation of Visual Studio Code.
Then, you can see the project code's file directory in Visual Studio Code.
You code the pages in the pages
directory, and configure the page routes in the file src/routes.config.ts
. Example:
The other files will be described in subsequent development steps.
For more information, see the SDM documentation.
src/devices/schema.ts
file and set as const
after the generated Schema list for type hinting. And use the deviceSchema
type in src/devices/index.ts
.src/devices/index
, you can see the initialization logic of SDM, which is simplified as follows:// Create SDM
const value = new SmartDeviceModel({ deviceId });
value.init(); // Perform initialization
// Initialization completion event
value.onInitialized(() => {
// After initialization is completed, get the device information and operate the device
// For example, value.getDpSchema()
// ...
});
src/app.tsx
, set the application root component as follows, and use SdmProvider
to place the SDM instance in the Context:import { SdmProvider } from "@ray-js/panel-sdk";
import { devices } from "@/devices";
import { initPanelEnvironment } from "@ray-js/ray";
initPanelEnvironment({ useDefaultOffline: true });
class App extends React.Component {
render() {
return (
<SdmProvider value={devices.common}>{this.props.children}</SdmProvider>
);
}
}
src/pages/home/index.tsx
: import React from "react";
import { useDpSchema, useProps } from "@ray-js/panel-sdk";
import { View } from "@ray-js/ray";
export default () => {
// When the project starts, automatically pull the product schema information corresponding to the productId on the developer platform
const dpSchema = useDpSchema();
// Read dpState data from the device model
const dpState = useProps(state => state); // Get all dpState
const switch = useProps(state => state.switch); // Get values where dpCode is switch
console.log("dpSchema", dpSchema); // Print to view the contents of dpSchema
console.log("dpState", dpState); // Print to view the contents of dpState
return <View>hello world</View>;
};
Note that when using useProps
, it is recommended to specify exactly which DP to read. For example, read the switch state as state.switch
. The component will only re-render when the switch
changes.Go to IDE > Debug > Console to see the printed devInfo
:
Get devInfo
from the code:
import { useDevInfo } from "@ray-js/panel-sdk";
const devInfo = useDevInfo();
console.log("devInfo:", devInfo);
deviceInfo
The Console panel outputs much information. The most important data is devInfo
that is the device information.
devInfo
includes the following key fields:
codeIds
: an object that consists of dpCode
as the key and dpId
as the value.devId
: the device ID. It starts with vdevo
for a virtual device.deviceOnline
: indicates whether the device is online.dps
: an object that consists of dpId
as the key and DP status as the value.idCodes
: an object that results from reversing codeIds
.panelConfig
: the panel configurations, including bic
as advanced functions.productId
: the product that is bound with the device.schema
: defines the DPs of the product and details the code
, type, value range, and icon of each DP.state
: an object that consists of dpCode
as the key and DP status as the value.ui
: the uiId
of the panel.Typically, the useDevInfo
API is used to read devInfo
from the code. DPs are defined on the Tuya Developer Platform.
You can get the product schema from useDpSchema
:
import { useDpSchema, useProps } from "@ray-js/panel-sdk";
const dpSchema = useDpSchema(); // DP schema
const dpState = useProps((state) => state); // DP status
Traverse the schema to get all DPs and attributes of the product.
schema.map((item) => {
// item.code
// item.property
// ...
return <Text>{item.name}</Text>;
});
Based on item.property.type
, you can determine and render the UI display of different types of DPs.
rpx
unitThe styles in Ray development are defined with Less and support CSS modules. Create a file named index.module.less
in the following directory: src/pages/home/index.module.less
.
.container {
border-radius: 24rpx;
background-color: #fff;
margin-bottom: 24rpx;
}
The unit rpx
is a special unit used for the Ray framework to adapt to different devices.
Use styles in the following example:
import React from "react";
import { useDpSchema } from "@ray-js/panel-sdk";
import { View } from "@ray-js/components";
import styles from "./index.module.less"; // Note the method to import styles
export default () => {
const dpSchema = useDpSchema();
// Add className
return <View className={styles.container}>hello world</View>;
};
The Console panel outputs the logs that show the structure of the schema in the following format:
For more information, see Custom Functions.
Write the following example in src/home/index.tsx
:
import React from "react";
import { useDevInfo, useDpSchema, useProps } from "@ray-js/panel-sdk";
import { View } from "@ray-js/components";
export default () => {
const dpSchema = useDpSchema();
const dpState = useProps((state) => state);
console.log("dpSchema", dpSchema);
console.log("dpState", dpState);
return (
<View>
{Object.keys(dpSchema || {}).map((dpCode) => {
// Traverse and render each DP
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
Compile the program to see all DP codes and device status in the previewed panel in the left-side pane of the IDE.
The Console panel in the IDE shows the data structure of boolean DPs in the following example:
{
dptype: "obj";
id: "39";
type: "bool";
}
Use the switch component TySwitch
provided by the package @ray-js/components-ty
to render data, and run the following command to install the extension component library provided by Ray:
yarn add @ray-js/components-ty
TySwitch
Write the following example:
For boolean DPs, a toggle
method can be called in actions
to switch the DP value between true
and false
.
import React from "react";
import { useDpSchema } from "@ray-js/panel-sdk";
import { TySwitch } from "@ray-js/components-ty";
import { View } from "@ray-js/components";
const BoolCard = ({ dpCode }) => {
const dpValue = useProps((state) => state[dpCode]);
const actions = useActions();
return (
<View>
{dpCode}:{" "}
<TySwitch
checked={dpValue}
onChange={() => {
actions[dpCode].toggle();
}}
/>
</View>
);
};
export default () => {
const dpSchema = useDpSchema();
const dpState = useProps((state) => state);
console.log("dpSchema", dpSchema);
return (
<View>
{Object.keys(dpSchema || {}).map((dpCode) => {
const props = dpSchema[dpCode];
// If it is a boolean type, return TySwitch
if (props.type === "bool") {
return <BoolCard dpCode={dpCode} />;
}
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
Compile the program. Then, the IDE renders all DPs of boolean type as switch components.
During the function definition, a DP already has multilingual texts in the product information. In the following example, the Strings tool in src/i18n
is used to get the DP text. Write the following code:
import React from "react";
import { useDpSchema, useProps } from "@ray-js/panel-sdk";
import { TySwitch } from "@ray-js/components-ty";
import { View } from "@ray-js/components";
import Strings from "@/i18n";
export default () => {
const dpSchema = useDpSchema();
const dpState = useProps((state) => state);
console.log("dpSchema", dpSchema);
return (
<View>
{Object.keys(dpSchema || {}).map((dpCode) => {
const props = dpSchema[dpCode];
if (props.type === "bool") {
// Use the Strings.getDpLang method to get multilingual text
return (
<View>
{Strings.getDpLang(dpCode)}:{" "}
<TySwitch checked={dpState[dpCode]} />
</View>
);
}
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
Compile the program. Then, the IDE renders all DPs of boolean type in Chinese.
Now, all DPs of boolean type are rendered.
However, DPs must be sent if you want to control devices. For this purpose, you can use SDM hooks useActions
to implement DP sending. Example:
import { useActions } from "@ray-js/panel-sdk";
const actions = useActions();
// Send DPs to the specified device. xxx in actions.xxx is dpCode, and set is the method of sending commands. Pass in dpValue to invoke and send the commands.
actions.switch.set(true); // Send a DP and set switch to true
actions.switch.on(); // Turn on DPs of boolean type
actions.switch.off(); // Turn off DPs of boolean type
actions.switch.toggle(); // Toggle the boolean value between true and false
This is an example of how to write code to send switch DPs.
First, implement the boolean DP sending and reporting component.
import React from "react";
import { useActions, useProps } from "@ray-js/panel-sdk";
import { View } from "@ray-js/components";
const BoolCard = ({ dpCode }) => {
const actions = useActions();
const dpValue = useProps((state) => state[dpCode]);
return (
<View>
{dpCode}:{" "}
<TySwitch checked={dpValue} onChange={() => actions[dpCode].toggle()} />
</View>
);
};
For boolean DPs, taking the actions
of switch DP as an example, actions.xxx
applies the following tool methods:
actions.switch.on; // Send the switch DP with a value of true
actions.switch.off; // Send the switch DP with a value of false
actions.switch.toggle; // Toggle between true and false
export default () => {
const schema = useDpSchema();
const dpState = useProps((state) => state);
const actions = useActions();
return (
<View>
{Object.keys(schema || {}).map((dpCode) => {
const props = schema[dpCode];
// If it is a boolean type, return TySwitch
if (props.type === "bool") {
return <BoolCard key={dpCode} dpCode={dpCode} />;
}
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
After the program is compiled in the IDE, use the virtual device plugin to debug the program. Go to Virtual Device > Virtual Panel and click a switch in the debugger. During the debugging, the switch in the simulator receives a DP command and changes to the respective switch state.
The schema for the DPs of enum type includes the following properties:
{
dptype: "obj";
id: "20";
range: ["cancel", "1h", "2h", "3h", "4h", "5h", "6h"];
type: "enum";
}
TyActionsheet
dialog component and TyCell
list componentThe items in the range can be rendered with the ActionSheet
dialog component and Cell
list component from @ray-js/components-ty
. Write the following code:
First, implement the enum display control component.
import React from "react";
import { useActions, useProps } from "@ray-js/panel-sdk";
import { View } from "@ray-js/components";
import Strings from "@/i18n";
const EnumCard = ({ dpCode }) => {
const actions = useActions();
const dpValue = useProps((state) => state[dpCode]);
const [showEnumDp, setShowEnumDp] = useState(null);
return (
<View>
{Strings.getDpLang(dpCode)}:
<Button onClick={() => setShowEnumDp(dpCode)}>
{Strings.getDpLang(dpCode, dpState[dpCode])}
</Button>
<TyActionsheet
header={Strings.getDpLang(dpCode)}
show={showEnumDp === dpCode}
onCancel={() => setShowEnumDp(null)}
>
<View style={{ overflow: "auto", height: "200rpx" }}>
<TyCell.Row
rowKey="title"
dataSource={props.range.map((item) => ({
title: Strings.getDpLang(dpCode, item),
onClick: () => {
actions[dpCode].set(item);
},
}))}
/>
</View>
</TyActionsheet>
</View>
);
};
For enum DPs, taking the actions
of work_mode
DP as an example, actions.xxx
applies the following tool methods:
actions.work_mode.set("white"); // Send work_mode with a value of white
actions.work_mode.next(); // Switch to the next enum value
actions.work_mode.prev(); // Switch to the previous enum value
actions.work_mode.random(); // Switch to a random enum value
Traverse schema rendering:
import React, { useState } from "react";
import { useDpSchema, useProps } from "@ray-js/panel-sdk";
import { TyActionsheet, TyCell, TySwitch } from "@ray-js/components-ty";
import { Button, ScrollView, View } from "@ray-js/components";
import Strings from "@/i18n";
export default () => {
const schema = useDpSchema();
const dpState = useProps((state) => state);
console.log("schema", schema);
const [showEnumDp, setShowEnumDp] = useState(null);
return (
<View>
{Object.keys(schema).map((dpCode) => {
const props = schema[dpCode];
// For the sake of length, other types are ignored here. You can copy and paste the local code of boolean type in the previous section here.
if (props.type === "enum") {
return <EnumCard key={dpCode} dpCode={dpCode} />;
}
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
Click the button to trigger an ActionSheet
dialog that uses the Cell
list component to render multiple enumerated items, as shown in the following result:
The schema for the DPs of string type includes the following properties:
{
dptype: "obj";
id: "20";
type: "string";
}
Input
componentFirst, implement the Input
component.
import React from "react";
import { useActions, useProps } from "@ray-js/panel-sdk";
import { Input } from "@ray-js/components";
import Strings from "@/i18n";
const InputCard = ({ dpCode }) => {
const actions = useActions();
const dpValue = useProps((state) => state[dpCode]);
const [showEnumDp, setShowEnumDp] = useState(null);
return (
<Input
value={dpState[dpCode]}
onInput={(event) => {
actions[dpCode].set(event.detail.value);
}}
/>
);
};
For string DPs, taking the actions
of name
DP as an example, actions.xxx
applies the following tool methods:
actions.name.set("my device"); // Send name with a value of my device
Traverse schema rendering:
import { useDpSchema, useActions } from "@ray-js/panel-sdk";
import { Input } from "@ray-js/components";
export default () => {
const schema = useDpSchema();
const dpState = useProps((state) => state);
console.log("schema", schema);
return (
<View>
{Object.keys(schema || {}).map((dpCode) => {
const props = schema[dpCode];
// For the sake of length, other types are ignored here. You can copy and paste the local code in the previous section here.
if (props.type === "string") {
return <InputCard value={dpState[dpCode]} />;
}
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
DPs of raw type generally do not need to be rendered. If necessary, they can be rendered in the same way as that of string type.
The schema for the DPs of value type includes the following properties:
{
dptype: "obj";
id: "18";
max: 100;
min: 0;
scale: 0;
step: 1;
type: "value";
unit: "%";
}
First, implement the Slider component.
The following example defines the range of values and units to be rendered with the Slider component:
import React from "react";
import { useActions, useProps } from "@ray-js/panel-sdk";
import { Slider } from "@ray-js/components";
import Strings from "@/i18n";
const ValueCard = ({ dpCode }) => {
const actions = useActions();
const dpValue = useProps((state) => state[dpCode]);
return (
<View>
{Strings.getDpLang(dpCode)}:
<Slider
step={props?.step}
max={props?.max}
min={props?.min}
value={dpState[dpCode]}
onChange={(event) => {
actions[dpCode].set();
}}
/>
</View>
);
};
For value DPs, taking the actions
of add_ele
electricity consumption DP as an example, actions.xxx
applies the following tool methods:
actions.add_ele.set(1000); // Send add_ele with a value of 1000
actions.add_ele.inc(); // Progressively increases by the step of the current DP
actions.add_ele.dec(); // Progressively decreases by the step of the current DP.
Traverse schema rendering:
import { useDpSchema, useActions, useProps } from "@ray-js/panel-sdk";
import { Slider } from "@ray-js/components";
export default () => {
const schema = useDpSchema();
const dpState = useProps((state) => state);
console.log("schema", schema);
return (
<View>
{Object.keys(schema).map((dpCode) => {
const props = schema[dpCode];
// For the sake of length, other types are ignored here. You can copy and paste the local code in the previous section here.
if (props.type === "value") {
return <ValueCard dpCode={dpCode} />;
}
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
The Slider component is a slider that limits a value range. As rendered in the IDE, all DPs of value type are rendered as sliders.
A bitmap is used to report faults and includes the following properties:
{
dptype: "obj";
id: "22";
label: ["sensor_fault", "temp_fault"];
maxlen: 2;
type: "bitmap";
}
Use the Notification
component provided by @ray-js/ray-components-plus
to render the DPs of bitmap type:
import { useDpSchema, useActions, useProps } from "@ray-js/panel-sdk";
import { Notification } from "@ray-js/ray-components-plus";
export default () => {
const schema = useDpSchema();
const dpState = useProps((state) => state);
console.log("schema", schema);
// Notification messages are handled in useEffect
useEffect(() => {
Object.keys(schema).forEach((dpCode) => {
const props = schema[dpCode];
if (props.type === "bitmap") {
Notification.show({
message: Strings.getFaultStrings(dpCode, dpState[dpCode]),
icon: "warning",
});
}
});
}, [schema, dpState]);
return (
<View>
{Object.keys(schema).map((dpCode) => {
const props = schema[dpCode];
// For the sake of length, other types are ignored here. You can copy and paste the local code in the previous section here.
return (
<View key={dpCode}>
{dpCode}: {dpState[dpCode]}
</View>
);
})}
</View>
);
};
Bitmaps are usually used to report faults by means of notifications. Traverse the schema in useEffect
to find bitmaps and generate dialogs.
The above schema contains the standard DPs for home appliances. Custom DPs can be added.
Create a DP of boolean type.
Return to the IDE, click the Compile button, and then refresh the panel to see that the newly-created DP is rendered.
When a panel is running, it must get device information to display the running status of the device. During panel development, if you do not have a real device, use a virtual device to finish debugging. The result is the same as that when a real device is used. For panel miniapps, virtual devices are equivalent to real devices. The following figure shows their relationship.
Before a virtual device is used, make sure your app account is authorized to log in to the IDE. In the top right corner of the IDE, use the Smart Life app to scan the QR code and authorize login.
After login, open the Universal Panel project, click the debugging tool Virtual Device, and then use the Smart Life app to scan the QR code and create a virtual device.
The Virtual Device panel is divided into three sections. The fallback control panel on the left displays the DPs of the current product. The list of DPs on the right is used to control the virtual device. You can click a DP and click Report to simulate DP reporting. The Log panel outputs MQTT logs. Click the Virtual Panel tab and switch to the fallback control panel view that provides the same features as the Control Panel tab but with a more intuitive view of product DPs.
Debug the program you have written.
Go to the page for debugging with Virtual Device, find Device Information > deviceId, and then copy the device ID.
In the top menu bar of the IDE, click Compilation Parameters, and enter the debugging parameters in the required format. Example: deviceId=vdevo165398364416684
.
After you set compilation parameters, click Remote Debugger in the top menu bar. The mobile phone for testing must have the Smart Life app downloaded and installed.
Use the Smart Life app to scan the QR code and open the panel miniapp. Then, you can debug the universal panel.
Now, you have walked through the rendering and sending of six types of DPs. Here is the open source address of the universal panel code: panel-universal.
After style optimization, the universal panel is displayed as shown in the following figure.
After debugging in the IDE, click Upload Source Code, enter the version number and description, and then click Upload.
After the package is uploaded, go to Smart MiniApp Developer Platform > Versions to see the uploaded version.
For a panel miniapp that does not belong to an official entity, before you submit the version for review, complete its information to be displayed on the Tuya Developer Platform. For more information, see Submit for Review.