Use panel miniapp to build a light source panel supporting standard capabilities of cool white light (C), cool and warm light (CW), colored light (RGB), cool white and colored light (RGBC), and white and colored light (RGBCW) and use smart device models and functional pages to implement the following capabilities:
For more information, see Panel MiniApp > Set up environment.
Product name: C, CW, RGB, RGBC, and RGBCW light
The light supports C, CW, RGB, RGBC, and RGBCW modes based on the data points of the white light mode and multicolor light mode.
Definition:
colour_data, white light brightness bright_value, and white light color temperature temp_value.switch_led by tapping on the button in the middle of the homepage.colour_data, bright_value, and temp_value data points are configured, an RGBCW product supporting colored light, white light brightness, and white light color temperature is displayed.colour_data and bright_value data points are configured, an RGBC product supporting colored light and white light brightness is displayed.colour_data data point is configured, an RGB product supporting colored light is displayed.bright_value and temp_value data points are configured, a CW product supporting white light brightness and white light color temperature is displayed.bright_value data point is configured, a C product supporting white light brightness is displayed.
control_data data point (if configured) at a throttled interval of 300 ms when users hold the slider, and the colour_data data point is sent when users drop the slider.
colour_data data point.
temp_value data point using the control_data data point. The second slider controls brightness and sends the bright_value data point using the control_data data point.
bright_value and temp_value data points.




countdown data point to the device, and the device automatically turns on or off after a specified duration.


music_data data point, creating a vibrant, music-responsive lighting experience.
The light source template must have the following data points. Either bright_value or colour_data (both related to dimming) must be included. Additional data points can be selected as needed based on product requirements.
switch_led
work_mode
bright_value
colour_data
DP ID | DP name | Identifier | Data transmission type | Data type | DP property |
20 | Switch | switch_led | Send and report (read-write) | Boolean | - |
21 | Mode | work_mode | Send and report (read-write) | Enum | Enum values: white, colour, scene, music |
22 | White light brightness | bright_value | Send and report (read-write) | Value | Value range: 10–1000, interval: 1, multiple: 0, unit: |
23 | Color temperature | temp_value | Send and report (read-write) | Value | Value range: 0–1000, interval: 1, multiple: 0, unit: |
24 | Multicolor light | colour_data | Send and report (read-write) | String | - |
25 | Scene | scene_data | Send and report (read-write) | String | - |
26 | Countdown | countdown | Send and report (read-write) | Value | Value range: 0–86400, interval: 1, multiple: 0, unit: s |
27 | Dance to music | music_data | Send only (write-read) | String | - |
28 | Real-time adjustment | control_data | Send only (write-read) | String | - |
30 | Biorhythm | rhythm_mode | Send and report (read-write) | Raw | - |
33 | Power-off memory | power_memory | Send and report (read-write) | Raw | - |
34 | Do-Not-Disturb | do_not_disturb | Send and report (read-write) | Boolean | - |
35 | Light on/off gradual change | switch_gradient | Send and report (read-write) | Raw | - |
For data points with the data type of Raw, see the specific protocol for detailed development instructions.

Create a light source, define data points, and configure the data points in the panel.
Log in to Tuya Developer Platform and create the light source.


🎉 By now, a light source is created.
Log in to Tuya MiniApp Developer Platform to create a panel miniapp.
For more information, see Panel MiniApp > Create panel miniapp.
For the repository of the light source template, see GitHub Repositories.





By now, the initialization of the development of a panel miniapp is completed. Before implementing dimming control, review the introduction to the project directory.
.
├── public # Static resource directory.
│ └── images
├── ray.config.ts # Ray configuration file.
├── src
│ ├── api # Defines general APIs.
│ ├── components # Defines general components.
│ ├── config # Defines general configurations.
│ ├── constant # Defines general constants.
│ ├── devices # Defines the current device model, used with SDM.
│ ├── hooks # Defines general hooks.
│ ├── i18n # Defines multilingual support for the current project.
│ ├── pages # Defines page components for the current project.
│ ├── redux # Directory of the status management tool Redux.
│ ├── utils # Defines general utility methods.
│ ├── app.config.ts # Auto-generated configuration file (do not modify).
│ ├── app.less # Less configuration for the miniapp.
│ ├── app.tsx # Root component.
│ ├── composeLayout.tsx # Component for general business logic processing.
│ ├── global.config.ts # Global configuration file for the miniapp.
│ ├── routes.config.ts # Route configuration file for the miniapp.
│ └── variables.less # Less variables for the miniapp.
├── typings # Definition directory of business types.
│ ├── lamp.d.ts # Definition files of lighting service general type.
│ ├── sdm.d.ts # Definition files of smart device type.
Reference code: src/devices/schema.ts

Generate SDM schema for the project by referring to src/devices/schema.ts. Then use hooks such as useProps, useActions, useStructuredProps, and useStructuredActions to enable automatic type inference based on the schema generated by the product definition file.
export const defaultSchema = [
{
attr: 641,
canTrigger: true,
code: 'switch_led',
defaultRecommend: true,
editPermission: false,
executable: true,
extContent: '',
iconname: 'icon-dp_power',
id: 20,
mode: 'rw',
name: 'Switch',
property: {
type: 'bool',
},
type: 'obj',
},
{
attr: 640,
canTrigger: true,
code: 'work_mode',
defaultRecommend: true,
editPermission: false,
executable: true,
extContent: '',
iconname: 'icon-dp_mode',
id: 21,
mode: 'rw',
name: 'Mode',
property: {
range: ['white', 'colour', 'scene', 'music'],
type: 'enum',
},
type: 'obj',
},
// ...
] as const; // Note: Use assertion statements to enhance TypeScript type hints.
Reference code: src/components/ControlBar/index.tsx
useProps to retrieve the status of switch_led and display different components according to the status.useActions to send the status of switch_led when the on/off button component is tapped.Example:
import React from 'react';
import { View } from '@ray-js/ray';
import { useProps, useActions } from '@ray-js/panel-sdk';
export default function() {
// Get the on/off status of the current device.
const power = useProps(props => props.switch_led);
const actions = useActions();
const handleTogglePower = React.useCallback(() => {
actions.switch_led.toggle({ throttle: 300 });
}, []);
return (
<View style={{ flex: 1 }}>
<View>switch_led: {power}</View>
<View onClick={handleTogglePower}>Tap to change the on/off status.</View>
</View>
);
}
Reference code: src/pages/Home/index.tsx and src/components/Dimmer/Colour/index.tsx
useStructuredProps to get the hue, saturation, and value data from colour_data.useStructuredActions to send specific data in colour_data when the sliders are adjusted.For more information about the integration, see Smart Device Model > Interceptors > dp-kit.
Example:
import React from 'react';
import { View } from '@ray-js/ray';
import { useProps, useActions } from '@ray-js/panel-sdk';
import {
LampColorSlider,
LampSaturationSlider,
LampBrightSlider,
} from '@ray-js/components-ty-lamp';
export default function() {
const colour = useStructuredProps(props => props.colour_data);
const dpStructuredActions = useStructuredActions();
const handleTouchEnd = React.useCallback(
(type: 'hue' | 'saturation' | 'value') => {
return (v: number) => {
dpStructuredActions[code].set(value, {
throttle: 300,
immediate: true,
});
};
},
[colour]
);
return (
<View style={{ flex: 1 }}>
<LampColorSlider value={colour?.hue ?? 1} onTouchEnd={handleTouchEnd('hue')} />
<LampSaturationSlider
hue={colour?.hue ?? 1}
value={colour?.saturation ?? 1}
onTouchEnd={handleTouchEnd('saturation')}
/>
<LampBrightSlider value={colour?.value ?? 1} onTouchEnd={handleTouchEnd('value')} />
</View>
);
}
Reference code: src/composeLayout.tsx
On the panel, use a custom key in the root component to query the data of favorite colors for the current device. Cache the retrieved data to both the cloud and the app's local storage using the device storage capability by referring to Storage. Use Redux to maintain the global data of favorite colors for convenient reuse across pages and components.
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import { showLoading, hideLoading } from '@ray-js/ray';
import { utils } from '@ray-js/panel-sdk';
import store from '@/redux';
import { devices, dpKit } from '@/devices';
import defaultConfig from '@/config/default';
import { initCloud } from './redux/modules/cloudStateSlice';
import './styles/index.less';
import { CLOUD_DATA_KEYS_MAP } from './constant';
const { defaultColors, defaultWhite } = defaultConfig;
interface Props {
devInfo: DevInfo;
extraInfo?: Record<string, any>;
preload?: boolean;
}
interface State {
devInfo: DevInfo;
}
const composeLayout = (Comp: React.ComponentType<any>) => {
return class PanelComponent extends Component<Props, State> {
async onLaunch() {
devices.lamp.init();
devices.lamp.onInitialized(device => {
dpKit.init(device);
this.initCloudData();
});
}
/**
* Initialize cached device data in the cloud and sync the data to Redux.
*/
async initCloudData() {
showLoading({ title: '' });
const storageKeys = [CLOUD_DATA_KEYS_MAP.collectColors, CLOUD_DATA_KEYS_MAP.collectWhites];
return Promise.all(storageKeys.map(k => devices.lamp.model.abilities.storage.get(k)))
.then(data => {
// Use the default value if no data is available in the cloud.
const cloudData = {
[CLOUD_DATA_KEYS_MAP.collectColors]: [...defaultColors],
[CLOUD_DATA_KEYS_MAP.collectWhites]: [...defaultWhite],
} as Parameters<typeof initCloud>['0'];
data.forEach((v, i) => {
const storageKey = storageKeys[i];
if (v) {
const value = utils.parseJSON(v);
cloudData[storageKey] = value?.data?.value;
}
});
store.dispatch(initCloud(cloudData));
hideLoading();
})
.catch(err => {
console.log('storage.get failed', err);
hideLoading();
});
}
render() {
const { extraInfo } = this.props;
return (
<Provider store={store}>
<Comp extraInfo={extraInfo} {...this.props} />
</Provider>
);
}
};
};
export default composeLayout;
Reference code: src/components/CollectColors/index.tsx
When adding or removing favorite colors, the corresponding data is sent to the cloud, as shown in the example code below. Redux and StorageAbility are used to manage and maintain global and cloud data of favorite colors.
Note: A maximum of 1,024 characters are allowed for stored device data, and it is recommended not to exceed 256 characters to avoid storage failures.
import React, { useState, useEffect } from 'react';
import _ from 'lodash-es';
import { View, showModal, showToast } from '@ray-js/ray';
import { utils, useSupport } from '@ray-js/panel-sdk';
import { useUnmount } from 'ahooks';
import clsx from 'clsx';
import { useSelector } from 'react-redux';
import Strings from '@/i18n';
import { Button } from '@/components';
import { devices } from '@/devices';
import { CLOUD_DATA_KEYS_MAP } from '@/constant';
import { ReduxState, useAppDispatch } from '@/redux';
import {
selectActiveIndex,
updateColorIndex,
updateWhiteIndex,
} from '@/redux/modules/uiStateSlice';
import {
selectCollectColors,
updateCollectColors,
updateCollectWhites,
} from '@/redux/modules/cloudStateSlice';
import styles from './index.module.less';
const { hsv2rgbString, brightKelvin2rgb } = utils;
const MAX_LENGTH = 8;
const MIN_LENGTH = 3;
interface IProps {
showAdd?: boolean;
style?: React.CSSProperties;
addStyle?: React.CSSProperties;
disable?: boolean;
isColor: boolean;
colourData: COLOUR;
brightness: number;
temperature: number;
chooseColor: (v: COLOUR & WHITE) => void;
addColor?: () => void;
deleteColor?: (v: any) => void;
}
export const CollectColors = (props: IProps) => {
const {
isColor,
chooseColor,
addColor,
addStyle,
deleteColor,
showAdd = true,
disable = false,
colourData,
brightness,
temperature,
style,
} = props;
const [animate, setAnimate] = useState(false);
const support = useSupport();
const dispatch = useAppDispatch();
const activeIndex = useSelector((state: ReduxState) => selectActiveIndex(state, isColor));
const collectColors = useSelector((state: ReduxState) => selectCollectColors(state, isColor));
useEffect(() => {
if (isColor) {
const newColorIndex = collectColors?.findIndex(item => {
const { hue: h, saturation: s, value: v } = item;
const { hue, saturation, value } = colourData;
return h === hue && saturation === s && value === v;
});
newColorIndex ! == activeIndex && dispatch(updateColorIndex(newColorIndex));
} else if (support.isSupportTemp()) {
const newWhiteIndex = collectColors?.findIndex(item => {
const { brightness: b, temperature: t } = item;
return b === brightness && t === temperature;
});
newWhiteIndex ! == activeIndex && dispatch(updateWhiteIndex(newWhiteIndex));
} else {
const newWhiteIndex = collectColors?.findIndex(item => {
const { brightness: b } = item;
return b === brightness;
});
newWhiteIndex ! == activeIndex && dispatch(updateWhiteIndex(newWhiteIndex));
}
}, [colourData, temperature, brightness, isColor, collectColors]);
useUnmount(() => {
setAnimate(false);
});
const handleAddColor = () => {
if (activeIndex > -1) {
showModal({
title: Strings.getLang('repeatColor'),
cancelText: Strings.getLang('cancel'),
confirmText: Strings.getLang('confirm'),
});
} else {
const newList = [...collectColors, isColor ? { ...colourData } : { brightness, temperature }];
const storageKey = isColor
? CLOUD_DATA_KEYS_MAP.collectColors
: CLOUD_DATA_KEYS_MAP.collectWhites;
devices.lamp.model.abilities.storage
.set(storageKey, newList)
.then(() => {
if (isColor) {
dispatch(updateColorIndex(newList.length - 1));
dispatch(updateCollectColors(newList as ReduxState['cloudState']['collectColors']));
} else {
dispatch(updateWhiteIndex(newList.length - 1));
dispatch(updateCollectWhites(newList as ReduxState['cloudState']['collectWhites']));
}
})
.catch(err => {
showToast({ title: Strings.getLang('addColorFailed') });
console.log('storage.save addColor failed', err);
});
}
addColor?.();
};
const handleDeleteColor = item => {
const newList = _.cloneDeep(collectColors);
if (activeIndex > -1) {
newList.splice(activeIndex, 1);
const storageKey = isColor
? CLOUD_DATA_KEYS_MAP.collectColors
: CLOUD_DATA_KEYS_MAP.collectWhites;
devices.lamp.model.abilities.storage
.set(storageKey, newList)
.then(() => {
if (isColor) {
dispatch(updateColorIndex(-1));
dispatch(updateCollectColors(newList as ReduxState['cloudState']['collectColors']));
} else {
dispatch(updateWhiteIndex(-1));
dispatch(updateCollectWhites(newList as ReduxState['cloudState']['collectWhites']));
}
})
.catch(err => {
console.log('storage.save deleteColor failed', err);
});
}
deleteColor?.(item);
};
const handleChoose = (item, index) => {
if (!disable) {
chooseColor(item);
if (isColor) dispatch(updateColorIndex(index));
else dispatch(updateWhiteIndex(index));
setAnimate(true);
}
};
let isAddEnabled = collectColors.length < MAX_LENGTH && showAdd;
if (!isColor && !support.isSupportTemp()) isAddEnabled = false; // You cannot add favorite colors when the white light mode is selected, and only brightness can be set.
return (
<View className={styles.row} style={style}>
{isAddEnabled && (
<Button
img="/images/add_icon.png"
imgClassName={styles.icon}
className={clsx(styles.button, styles.add)}
style={addStyle}
onClick={handleAddColor}
/>
)}
<View
className={styles.colorRow}
style={{
marginLeft: isAddEnabled ? 108 : 0,
}}
>
<View style={{ display: 'flex' }}>
{collectColors?.map((item, index) => {
const isActive = index === activeIndex;
const bg = isColor
? hsv2rgbString(
item.hue,
item.saturation / 10,
(200 + 800 * (item.value / 1000)) / 10
)
: brightKelvin2rgb(
200 + 800 * (item.brightness / 1000),
support.isSupportTemp() ? item.temperature : 1000,
{ kelvinMin: 4000, kelvinMax: 8000 }
);
return (
<View
key={index}
className={`${styles.circleBox} ${styles.button}`}
style={{
marginRight: index === collectColors.length - 1 ? 0 : '24rpx',
border: isActive ? `4rpx solid ${bg}` : 'none',
background: 'transparent',
opacity: disable ? 0.4 : 1,
}}
onClick={() => handleChoose(item, index)}
>
<View
className={styles.circle}
style={{
backgroundColor: bg,
transform: `scale(${isActive ? 0.7 : 1})`,
transition: animate ? 'all .5s' : 'none',
}}
/>
{index + 1 > MIN_LENGTH && isActive && (
<Button
img="/images/delete_icon.png"
imgClassName={styles.deleteIcon}
className={clsx(styles.button, styles.noBg)}
onClick={() => handleDeleteColor(item)}
/>
)}
</View>
);
})}
</View>
</View>
</View>
);
};
Reference code: src/components/Dimmer/White/index.tsx
The useSupport hook allows you to determine whether specific functions, such as color temperature and brightness adjustment, are supported by the current device. This enables dynamic adaptation of UI components. In the example code, the isSupportTemp method is used to control whether the color temperature slider is rendered. If the device does not have the color temperature data point, the slider will be hidden.
Note: Before using useSupport, ensure SmartSupportAbility is included in the device definition directory. For more information, see src/devices/index.ts.
import React from 'react';
import { View } from '@ray-js/ray';
import { useProps, useActions, useSupport } from '@ray-js/panel-sdk';
import { LampTempSlider } from '@ray-js/components-ty-lamp';
export default function SupportDemo() {
const support = useSupport();
const temperature = useProps(dpState => dpState.temp_value);
const actions = useActions();
const handleTouchEnd = React.useCallback(temp => {
actions.temp_value.set(temp);
}, []);
return (
<View style={{ flex: 1 }}>
{support.isSupportTemp() && (
<LampTempSlider value={temperature} onTouchEnd={handleTouchEnd} />
)}
</View>
);
}
To develop a panel that is adaptable for C, CW, RGB, RGBC, and RGBCW lights, use useSupport to identify the functions supported by the current device based on the product definitions for these lights and then render UI components based on the supported functions.
Reference code: src/global.config.ts
Inject the functionalPages configuration item into global.config.ts to specify the appid for the timing and countdown page. This implements jumping to the corresponding page with a single line of code.
import { GlobalConfig } from '@ray-js/types';
export const tuya = {
window: {
backgroundColor: 'black',
navigationBarTitleText: '',
navigationBarBackgroundColor: 'white',
navigationBarTextStyle: 'black',
},
functionalPages: {
// Timing and countdown page
rayScheduleFunctional: {
appid: 'tyjks565yccrej3xvo',
},
},
};
const globalConfig: GlobalConfig = {
basename: '',
};
export default globalConfig;
Reference code:
Use the navigateTo method for the direct jump to the timing and countdown page, as shown in the example code below. The openScheduleFunctional method is used to enable quick jumping to the functional page.
import { navigateTo } from '@ray-js/ray';
import { devices } from '@/devices';
import { getCachedLaunchOptions } from '@/api/getCachedLaunchOptions';
import { lampSchemaMap } from '@/devices/schema';
const { deviceId, groupId } = getCachedLaunchOptions()?.query ?? {};
export const openScheduleFunctional = async () => {
const { support } = devices.lamp.model.abilities;
const supportCountdown = support.isSupportDp(lampSchemaMap.countdown.code);
const supportCloudTimer = support.isSupportCloudTimer();
const supportRctTimer = false;
const url = `functional://rayScheduleFunctional/home?deviceId=${deviceId ||
''}&groupId=${groupId ||
''}&cloudTimer=${supportCloudTimer}&rtcTimer=${supportRctTimer}&countdown=${supportCountdown}`;
navigateTo({
url,
success(e) {
console.log('navigateTo openScheduleFunctional success', e);
},
fail(e) {
console.log('navigateTo openScheduleFunctional fail', e);
},
});
};