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