Developing Zigbee Gateways

Last Updated on : 2022-11-24 09:20:20download

This topic describes how to implement the functionality for Zigbee gateways. The implementation can be broken down into two parts. The basic part is to integrate with Tuya-specific Zigbee services for interconnection with all Powered by Tuya (PBT) Zigbee devices. The extended part is to achieve connection to third-party Zigbee devices outside of the PBT ecosystem.

Background

The SDK allows your gateway product to connect to all PBT Zigbee devices by integrating with Tuya’s Zigbee modules. The SDK also provides functions to enable your gateway to connect to third-party Zigbee devices, which can make your gateway more friendly for compatibility with Zigbee devices.

Zigbee basics

This section describes how to integrate with Tuya-specific Zigbee services to allow your gateway product to connect to all PBT Zigbee devices.

To achieve this, on the hardware, the microcontroller talks to Tuya’s Zigbee module via the serial port. On the software, the application calls specific functions to initialize and start the Zigbee services.

The SDK talks to the Zigbee module via the serial port. The SDK takes care of device connection and management, without the need for application implementation.

For the Zigbee initialization function, its parameters are in JSON format and used for configuration.

Field Description
storage_path The read/write path where the Zigbee data resides, such as the channel, PAN ID, and network key of the Zigbee coordinator as well as the endpoint, device ID, and cluster of the Zigbee sub-device.
cache_path The read/write path where the OTA firmware updates reside.
dev_name The name of the serial device.
cts Specifies whether to support hardware flow control.
thread_mode Specifies whether to use a thread to run the Zigbee host service.

Implement the Zigbee services based on the sample code provided in Quick Start.

// ...
#include "tuya_zigbee_api.h"
// ...

int main(int argc, char **argv)
{
    // ... 

    ty_cJSON *zb_cfg = ty_cJSON_CreateObject();
    if (zb_cfg == NULL) {
        return OPRT_CJSON_GET_ERR;
    }
    ty_cJSON_AddStringToObject(zb_cfg, "storage_path", "./");
    ty_cJSON_AddStringToObject(zb_cfg, "cache_path", "/tmp/");
    ty_cJSON_AddStringToObject(zb_cfg, "dev_name", "/dev/ttyS2");
    ty_cJSON_AddNumberToObject(zb_cfg, "cts", 1);
    ty_cJSON_AddNumberToObject(zb_cfg, "thread_mode", 1);

    /* initiate application service, more service in here */
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_init(zb_cfg));
    TUYA_CALL_ERR_RETURN(tuya_user_svc_init(&gw_cbs));

    // ...

    /* start application service, more service in here */
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_start(zb_cfg));
    TUYA_CALL_ERR_RETURN(tuya_user_svc_start(NULL));

    // ...

    return 0;
}

After your code is run, if the log looks like the following, the Zigbee service runs correctly. Theoretically, all the PBT Zigbee devices can be connected to the gateway.

// ...
[01-01 00:00:08:778 TUYA D][tuya_z3.c:26760] nodeEuiStr = 086bd7fffed0cb51.
[01-01 00:00:08:778 TUYA D][tuya_z3.c:26761] nodeId = 0x0000.
[01-01 00:00:08:778 TUYA D][tuya_z3.c:26762] panId = 0xc0a7.
[01-01 00:00:08:778 TUYA D][tuya_z3.c:26763] channel = 0x0f.
[01-01 00:00:08:778 TUYA D][tuya_z3.c:26764] ver = 1.0.9.
[01-01 00:00:08:778 TUYA D][tuya_z3.c:26765] netStatus = 0x03.
[01-01 00:00:08:778 TUYA D][tuya_z3.c:26766] manufacturerID = 0x0001.
[01-01 00:00:08:778 TUYA D][tuya_z3.c:26767] netWorkKey:
dd 47 74 57 a8 10 05 20 bd 99 f0 aa 08 ea 27 57
// ...

Zigbee extension

This section describes how to enable your gateway to connect to third-party Zigbee devices, which can make your gateway more friendly for compatibility with Zigbee devices.

To achieve this functionality, you need to add the manufacturer name and model ID of the third-party Zigbee device to the allowlist and register the list to the SDK. When a device requests to join the network, it will be validated against the allowlist. If this device is allowlisted, the SDK will trigger the device management callback. The code executed in the callback is implemented by you.

Basic knowledge of Zigbee protocol can help smooth out your development process. For more information, see Connecting to Sub-devices.

The following glossary defines some of the terms regarding sub-device management.

Term Explanation
SDK sub-device management Cloud-to-device communication. The cloud sends data to the SDK. The SDK forwards the received data to the application. The data communication is independent of the protocol the sub-device uses. You implement the protocol-specific logic in the callback.
Zigbee sub-device management Device-to-cloud communication. The Zigbee device sends data to the SDK. The SDK forwards the received data to the cloud. This is used to connect to third-party Zigbee devices. You implement the application logic in the callback.

Network joining

To enable the third-party Zigbee device to join the network:

  1. Register the allowlist as well as the Zigbee sub-device management callback.
  2. Register the SDK sub-device management callback.
  3. Open the gateway control panel on the mobile app and tap Add Sub-device. After receiving the joining request, the SDK allows the Zigbee device to join the network.
  4. Press the reset button on the Zigbee device to enter the pairing mode. The SDK checks if this device is allowlisted. If so, the SDK triggers the network joining callback in the Zigbee sub-device management. In the callback, the application parses the device information and binds the device with the cloud.
  5. After the Zigbee sub-device is bound with the cloud, the binding result callback in the SDK sub-device management will be invoked. In the callback, the application reads the status of the sub-device.
  6. When the Zigbee sub-device reports data, the data reporting callback in the Zigbee sub-device management will be invoked. In the callback, the application converts the Zigbee Cluster Library (ZCL) data into the data point (DP) data and reports data to the cloud.

The following sequence diagram shows the interaction in network joining:

Developing Zigbee Gateways

Implementation:

// ...
#include "tuya_zigbee_api.h"
#include "tuya_gw_subdev_api.h"
// ...

#define TY_TS_ONOF_LIGHT_ONE                    "TS0001"
#define TY_TS_ONOF_LIGHT_TWO                    "TS0002"
#define TY_TS_ONOF_LIGHT_THR                    "TS0003"

#define PROFILE_ID_HA                           0x0104
#define PROFILE_ID_LL                           0xC05E

#define ZCL_BASIC_CLUSTER_ID                    0x0000
#define ZCL_POWER_CONFIG_CLUSTER_ID             0x0001
#define ZCL_DEVICE_TEMP_CLUSTER_ID              0x0002
#define ZCL_IDENTIFY_CLUSTER_ID                 0x0003
#define ZCL_GROUPS_CLUSTER_ID                   0x0004
#define ZCL_SCENES_CLUSTER_ID                   0x0005
#define ZCL_ON_OFF_CLUSTER_ID                   0x0006

#define ZCL_CMD_TYPE_GLOBAL                     0x00
#define ZCL_CMD_TYPE_PRIVATE                    0x01

#define ZCL_FRAME_TYPE_UNICAST                  0x00
#define ZCL_FRAME_TYPE_MULTICAST                0x01
#define ZCL_FRAME_TYPE_BROADCAST                0x02

#define ZCL_OFF_COMMAND_ID                      0x00
#define ZCL_ON_COMMAND_ID                       0x01

#define ZCL_READ_ATTRIBUTES_COMMAND_ID          0x00
#define ZCL_READ_ATTRIBUTES_RESPONSE_COMMAND_ID 0x01
#define ZCL_REPORT_ATTRIBUTES_COMMAND_ID        0x0A
#define ZCL_READ_ATTRIBUTER_RESPONSE_HEADER     3 /* Attributer ID: 2 Bytes, Status: 1 Btye */
#define ZCL_REPORT_ATTRIBUTES_HEADER            2 /* Attributer ID: 2 Bytes */

#define DEVID_HA_IAS_ZONE                       0x0402    // 0x0402 -> 0x00  IAS Zone
#define DEVID_HA_TEMPERATURE_SENSOR             0x0302    // 0x0302 -> 0x01  Temperature Sensor
#define DEVID_HA_ONOF_LIGHT                     0x0100    // 0x0100 -> 0x02  On/off light
#define DEVID_HA_SMART_PLUG                     0x0051    // 0x0051 -> 0x03  Smart Plug

/* Custom DEVID, saved to UDDD */
#define TY_DEVID_HA_IAS_ZONE                    0x00      // 0x0402 -> 0x00  IAS Zone
#define TY_DEVID_HA_TEMPERATURE_SENSOR          0x01      // 0x0302 -> 0x01  Temperature Sensor
#define TY_DEVID_HA_ONOF_LIGHT                  0x02      // 0x0100 -> 0x02  On/off light
#define TY_DEVID_HA_SMART_PLUG                  0x03      // 0x0051 -> 0x03  Smart Plug

/* Custom TYPE, saved to UDDD */
#define TY_TYPE_PIR                             0x00      // DEVID_HA_IAS_ZONE
#define TY_TYPE_DOOR_SENSOR                     0x01      // DEVID_HA_IAS_ZONE
#define TY_TYPE_FIRE_SENSOR                     0x02      // DEVID_HA_IAS_ZONE
#define TY_TYPE_WATER_SENSOR                    0x03      // DEVID_HA_IAS_ZONE

#define TY_TYPE_ONOF_LIGHT_ONE                  0x00      // DEVID_HA_ONOF_LIGHT
#define TY_TYPE_ONOF_LIGHT_TWO                  0x01      // DEVID_HA_ONOF_LIGHT
#define TY_TYPE_ONOF_LIGHT_THREE                0x02      // DEVID_HA_ONOF_LIGHT

TY_Z3_DEV_S my_z3_dev[] = {
    { "TUYATEC-nPGIPl5D", "TS0001" },
    { "TUYATEC-1xAC7DHQ", "TS0002" },
};

TY_Z3_DEVLIST_S my_z3_devlist = {
    .devs = my_z3_dev,
    .dev_num = CNTSOF(my_z3_dev),
};

STATIC VOID __z3_onoff_light_read_attr(CONST CHAR_T *dev_id)
{
    OPERATE_RET op_ret = OPRT_OK;
    TY_Z3_APS_FRAME_S frame = {0};
    USHORT_T atrr_buf[5] = { 0x0000, 0x4001, 0x4002, 0x8001, 0x5000 };

    strncpy(frame.id, dev_id, SIZEOF(frame.id));
    frame.profile_id = PROFILE_ID_HA;
    frame.cluster_id = ZCL_ON_OFF_CLUSTER_ID;
    frame.cmd_type = ZCL_CMD_TYPE_GLOBAL;
    frame.src_endpoint = 0x01;
    frame.dst_endpoint = 0xff;
    frame.cmd_id = ZCL_READ_ATTRIBUTES_COMMAND_ID;

    frame.msg_length = SIZEOF(atrr_buf);
    frame.message = (UCHAR_T *)atrr_buf;

    op_ret = tuya_zigbee_send_data(&frame);
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_zigbee_send_data err: %d", op_ret);
        return;
    }
}

STATIC VOID __z3_onoff_light_report(TY_Z3_APS_FRAME_S *frame)
{
    OPERATE_RET op_ret = OPRT_OK;
    TY_OBJ_DP_S dp_data = {0};

    if (frame->cluster_id != ZCL_ON_OFF_CLUSTER_ID) {
        return;
    }

    dp_data.dpid = frame->src_endpoint;
    dp_data.type = PROP_BOOL;

    if (frame->cmd_id == ZCL_REPORT_ATTRIBUTES_COMMAND_ID) {
        dp_data.value.dp_bool = frame->message[ZCL_REPORT_ATTRIBUTES_HEADER+1];
    } else if (frame->cmd_id == ZCL_READ_ATTRIBUTES_RESPONSE_COMMAND_ID) {
        dp_data.value.dp_bool = frame->message[ZCL_READ_ATTRIBUTER_RESPONSE_HEADER+1];
    } else {
        return;
    }

    op_ret = dev_report_dp_json_async(frame->id, &dp_data, 1);
    if (op_ret != OPRT_OK) {
        PR_ERR("dev_report_dp_json_async err: %d", op_ret);
        return;
    }
}

STATIC VOID __my_z3_dev_report(TY_Z3_APS_FRAME_S *frame)
{
    OPERATE_RET op_ret = OPRT_OK;
    DEV_DESC_IF_S *dev_if = NULL;

    if (frame->profile_id != PROFILE_ID_HA) {
        return;
    }

    dev_if = tuya_iot_get_dev_if(frame->id);
    if (!dev_if || !dev_if->bind) {
        PR_ERR("dev_id: %s is not bind", frame->id);
        tuya_zigbee_del_dev(frame->id);
        return;
    }

    op_ret = tuya_iot_fresh_dev_hb(frame->id);
    if (op_ret != OPRT_OK) {
        PR_WARN("tuya_iot_fresh_dev_hb err: %d", op_ret);
    }

    UCHAR_T uc_devid = (((dev_if->uddd) >> 8) & 0xFF);
    switch (uc_devid)
    {
        case TY_DEVID_HA_ONOF_LIGHT:
            __z3_onoff_light_report(frame);
            break;
        case TY_DEVID_HA_IAS_ZONE:
        case TY_DEVID_HA_TEMPERATURE_SENSOR:
        case TY_DEVID_HA_SMART_PLUG:   
        default:
            PR_WARN("devid: %d is not support", uc_devid);
            return;
    }
}

STATIC VOID __my_z3_dev_join(TY_Z3_DESC_S *dev)
{
    OPERATE_RET op_ret = OPRT_OK;
    CHAR_T pid[PRODUCT_KEY_LEN+1]={0};
    CHAR_T sw_ver[SW_VER_LEN+1] = {0};
    USER_DEV_DTL_DEF_T uddd = 0;

    /**
     * The sample code is for demonstration only. You need to:
     *   a) Parse device information, extract the necessary information, and save it to the uddd.
     *   b) Call the device binding function to bind the device with the cloud by PID.
     */

    if (dev->profile_id[0] != PROFILE_ID_HA) {
        return;
    }

    CHAR_T iver1 = (CHAR_T)(dev->version & 0x0F); 
    CHAR_T iver2 = (CHAR_T)((dev->version >> 4) & 0x03);
    CHAR_T iver3 = (CHAR_T)((dev->version >> 6) & 0x03);
    snprintf(sw_ver, SIZEOF(sw_ver), "%d.%d.%d", iver3, iver2, iver1);

    switch (dev->device_id[0]) {
        case DEVID_HA_ONOF_LIGHT:
            uddd |= (TY_DEVID_HA_ONOF_LIGHT << 8);
            if (0 == strncasecmp(dev->model_id, TY_TS_ONOF_LIGHT_ONE, strlen(TY_TS_ONOF_LIGHT_ONE))) {
                uddd |= (TY_TYPE_ONOF_LIGHT_ONE & 0xFF);
                strncpy(pid, "ckj1pnvy", SIZEOF(pid));
            } else if (0 == strncasecmp(dev->model_id, TY_TS_ONOF_LIGHT_TWO, strlen(TY_TS_ONOF_LIGHT_TWO))) {
                uddd |= (TY_TYPE_ONOF_LIGHT_TWO & 0xFF);
                strncpy(pid, "tr4dshuh", SIZEOF(pid));
            } else {
                PR_WARN("model_id: %s is not supported", dev->model_id);
                return;
            }
            break;
        case DEVID_HA_IAS_ZONE:
        case DEVID_HA_TEMPERATURE_SENSOR:
        case DEVID_HA_SMART_PLUG:
        default:
            PR_WARN("devid: %d is not supported", dev->device_id[0]);
            return;
    }

    op_ret = tuya_iot_gw_bind_dev(GP_DEV_ATH_1, uddd, dev->id, pid, sw_ver);
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_iot_gw_bind_dev err: %d", op_ret);
        return;
    }
}

STATIC VOID __dev_bind_cb(CONST CHAR_T *dev_id, CONST OPERATE_RET result)
{
    OPERATE_RET op_ret = OPRT_OK;
    DEV_DESC_IF_S *dev_if = NULL;

    /**
     * The sample code is for demonstration only. You need to:
     *   a) Encapsulate the ZCL Read Attributes command based on device binding status and send it to the sub-device.
     *   b) Configure the heartbeat timeout.
     */

    if (result != OPRT_OK) {
        PR_ERR("dev_id: %s bind err", dev_id);
        tuya_zigbee_del_dev(dev_id);
        return;
    }

    op_ret = tuya_iot_set_dev_hb_cfg(dev_id, 120, 3, FALSE);
    if (op_ret != OPRT_OK) {
        PR_WARN("tuya_iot_set_dev_hb_cfg err: %d", op_ret);
    }

    op_ret = tuya_iot_fresh_dev_hb(dev_id);
    if (op_ret != OPRT_OK) {
        PR_WARN("tuya_iot_fresh_dev_hb err: %d", op_ret);
    }

    dev_if = tuya_iot_get_dev_if(dev_id);
    if (!dev_if || !dev_if->bind) {
        PR_ERR("dev_id: %s is not bind", dev_id);
        tuya_zigbee_del_dev(dev_id);
        return;
    }

    UCHAR_T uc_devid = (((dev_if->uddd) >> 8) & 0xFF);
    switch (uc_devid)
    {
        case TY_DEVID_HA_ONOF_LIGHT:
            __z3_onoff_light_read_attr(dev_id);
            break;
        case TY_DEVID_HA_IAS_ZONE:
        case TY_DEVID_HA_TEMPERATURE_SENSOR:
        case TY_DEVID_HA_SMART_PLUG:   
        default:
            PR_WARN("devid: %d is not support", uc_devid);
            return;
    }
}

STATIC OPERATE_RET __user_init(VOID)
{
    OPERATE_RET rt = OPRT_OK;
    /* Zigbee sub-device management callbacks */
    TY_Z3_DEV_CBS_S z3_dev_cbs = {
        .join        = __my_z3_dev_join,
        .report      = __my_z3_dev_report,
    };

    /* SDK sub-device management callbacks */
    TY_IOT_DEV_CBS_S dev_mgr_cbs = {
        .dev_bind_ifm  = __dev_bind_cb,
    };

    TUYA_CALL_ERR_RETURN(tuya_zigbee_custom_dev_mgr_init(&my_z3_devlist, &z3_dev_cbs));
    TUYA_CALL_ERR_RETURN(tuya_iot_reg_gw_mgr_cb(GP_DEV_ATH_1, &dev_mgr_cbs));

    return OPRT_OK;
}

int main(int argc, char **argv)
{
    // ... 

    ty_cJSON *zb_cfg = ty_cJSON_CreateObject();
    if (zb_cfg == NULL) {
        return OPRT_CJSON_GET_ERR;
    }
    ty_cJSON_AddStringToObject(zb_cfg, "storage_path", "./");
    ty_cJSON_AddStringToObject(zb_cfg, "cache_path", "/tmp/");
    ty_cJSON_AddStringToObject(zb_cfg, "dev_name", "/dev/ttyS2");
    ty_cJSON_AddNumberToObject(zb_cfg, "cts", 1);
    ty_cJSON_AddNumberToObject(zb_cfg, "thread_mode", 1);

    /* initiate application service, more service in here */
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_init(zb_cfg));
    TUYA_CALL_ERR_RETURN(tuya_user_svc_init(&gw_cbs));
    TUYA_CALL_ERR_RETURN(__user_init());

    // ...

    /* start application service, more service in here */
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_start(zb_cfg));
    TUYA_CALL_ERR_RETURN(tuya_user_svc_start(NULL));

    // ...

    return 0;
}

Device control

To control the third-party Zigbee device:

  1. Register the DP management callback. In the command handler callback, you need to convert the DP data into the ZCL data and send data to the sub-device.
  2. Register the Zigbee sub-device management callback. In the data reporting callback, you implement heartbeat refresh, convert the ZCL data into the DP data, and report data to the cloud.

The cloud takes the returned status as a response to command execution. After the sub-device executes the command, it should return the current status to the application. The application converts the ZCL data into the DP data and reports it to the cloud. If you want the third-party Zigbee device to proactively report the current status after command execution, the application should send a status query after sending the control command.

The following sequence diagram shows how they interact with each other:

Developing Zigbee Gateways

Implementation:

// ...

STATIC VOID __z3_onoff_light_write_attr(CONST TY_RECV_OBJ_DP_S *cmd)
{
    INT_T i= 0;
    OPERATE_RET op_ret = OPRT_OK;
    TY_Z3_APS_FRAME_S frame = {0};

    strncpy(frame.id, cmd->cid, SIZEOF(frame.id));
    frame.profile_id = PROFILE_ID_HA;
    frame.src_endpoint = 0x01;

    for (i = 0; i < cmd->dps_cnt; i++) {
        switch (cmd->dps[i].dpid) {
        case 1 ... 3:
            frame.dst_endpoint = cmd->dps[i].dpid;
            frame.cluster_id = ZCL_ON_OFF_CLUSTER_ID;
            frame.cmd_type = ZCL_CMD_TYPE_PRIVATE;
            frame.frame_type = ZCL_FRAME_TYPE_UNICAST;
            frame.msg_length = 0;
            if (cmd->dps[i].value.dp_bool) {
                frame.cmd_id = ZCL_ON_COMMAND_ID;
            } else {
                frame.cmd_id = ZCL_OFF_COMMAND_ID;
            }
            break;
        default:
            PR_WARN("dev_id: %s, dpid: %d is not supported", cmd->cid, cmd->dps[i].dpid);
            return;
        }

        op_ret = tuya_zigbee_send_data(&frame);
        if (op_ret != OPRT_OK) {
            PR_WARN("tuya_zigbee_send_data err: %d", op_ret);
        }
    }
}

STATIC VOID __z3_onoff_light_report(TY_Z3_APS_FRAME_S *frame)
{
    OPERATE_RET op_ret = OPRT_OK;
    TY_OBJ_DP_S dp_data = {0};

    if (frame->cluster_id != ZCL_ON_OFF_CLUSTER_ID) {
        return;
    }

    dp_data.dpid = frame->src_endpoint;
    dp_data.type = PROP_BOOL;

    if (frame->cmd_id == ZCL_REPORT_ATTRIBUTES_COMMAND_ID) {
        dp_data.value.dp_bool = frame->message[ZCL_REPORT_ATTRIBUTES_HEADER+1];
    } else if (frame->cmd_id == ZCL_READ_ATTRIBUTES_RESPONSE_COMMAND_ID) {
        dp_data.value.dp_bool = frame->message[ZCL_READ_ATTRIBUTER_RESPONSE_HEADER+1];
    } else {
        return;
    }

    op_ret = dev_report_dp_json_async(frame->id, &dp_data, 1);
    if (op_ret != OPRT_OK) {
        PR_ERR("dev_report_dp_json_async err: %d", op_ret);
        return;
    }
}

STATIC VOID __dev_cmd_obj_cb(CONST TY_RECV_OBJ_DP_S *cmd)
{
    DEV_DESC_IF_S *dev_if = NULL;

    /**
     * The sample code is for demonstration only. You need to:
     *   a) Get the device's UDDD and map it to the device type.
     *   b) Convert the DP data to the ZCL data by UDDD and send it to the sub-device.
     */

    dev_if = tuya_iot_get_dev_if(cmd->cid);
    if (!dev_if || !dev_if->bind) {
        PR_ERR("dev_id: %s is not bind", cmd->cid);
        return;
    }

    UCHAR_T uc_devid = (((dev_if->uddd) >> 8) & 0xFF);
    switch (uc_devid)
    {
        case TY_DEVID_HA_ONOF_LIGHT:
            __z3_onoff_light_write_attr(cmd);
            break;
        case TY_DEVID_HA_IAS_ZONE:
        case TY_DEVID_HA_TEMPERATURE_SENSOR:
        case TY_DEVID_HA_SMART_PLUG:   
        default:
            PR_WARN("devid: %d is not support", uc_devid);
            return;
    }
}

STATIC VOID __my_z3_dev_report(TY_Z3_APS_FRAME_S *frame)
{
    OPERATE_RET op_ret = OPRT_OK;
    DEV_DESC_IF_S *dev_if = NULL;

    if (frame->profile_id != PROFILE_ID_HA) {
        return;
    }

    dev_if = tuya_iot_get_dev_if(frame->id);
    if (!dev_if || !dev_if->bind) {
        PR_ERR("dev_id: %s is not bind", frame->id);
        tuya_zigbee_del_dev(frame->id);
        return;
    }

    op_ret = tuya_iot_fresh_dev_hb(frame->id);
    if (op_ret != OPRT_OK) {
        PR_WARN("tuya_iot_fresh_dev_hb err: %d", op_ret);
    }

    UCHAR_T uc_devid = (((dev_if->uddd) >> 8) & 0xFF);
    switch (uc_devid)
    {
        case TY_DEVID_HA_ONOF_LIGHT:
            __z3_onoff_light_report(frame);
            break;
        case TY_DEVID_HA_IAS_ZONE:
        case TY_DEVID_HA_TEMPERATURE_SENSOR:
        case TY_DEVID_HA_SMART_PLUG:   
        default:
            PR_WARN("devid: %d is not support", uc_devid);
            return;
    }
}

STATIC OPERATE_RET __user_init(VOID)
{
    OPERATE_RET rt = OPRT_OK;
    /* Zigbee sub-device management callbacks */
    TY_Z3_DEV_CBS_S z3_dev_cbs = {
        .join        = __my_z3_dev_join,
        .report      = __my_z3_dev_report,
    };

    /* SDK sub-device management callbacks */
    TY_IOT_DEV_CBS_S dev_mgr_cbs = {
        .dev_bind_ifm  = __dev_bind_cb,
    };

    TY_IOT_DP_CBS_S dev_dp_cbs = {
        .obj   = __dev_cmd_obj_cb,
    };

    TUYA_CALL_ERR_RETURN(tuya_zigbee_custom_dev_mgr_init(&my_z3_devlist, &z3_dev_cbs));
    TUYA_CALL_ERR_RETURN(tuya_iot_reg_gw_mgr_cb(GP_DEV_ATH_1, &dev_mgr_cbs));
    TUYA_CALL_ERR_RETURN(tuya_iot_reg_dp_cb(DP_DEV, GP_DEV_ATH_1, &dev_dp_cbs));

    return OPRT_OK;
}

// ...

Heartbeat management

The heartbeat mechanism monitors the connection between the gateway and sub-devices. There are two methods to achieve the heartbeat check.

  1. The active method. The sub-device proactively reports data to the gateway regularly.
  2. The passive method. The gateway regularly requests the status of the sub-device such as the version number and waits for a response.

For the active method, when the application receives data from the third-party Zigbee device, it will update the heartbeat status of this device.

For the passive method, the SDK takes care of the heartbeat check so the application only needs to configure the heartbeat timeout and timeout processing.

To implement the passive heartbeat check:

  1. Register the SDK sub-device management callback. Query the status of the sub-device in the heartbeat callback and set the sub-device timeout in the binding result callback.
  2. Register the Zigbee sub-device management callback. Update the heartbeat status of the sub-device in the data reporting callback and set the heartbeat timeout for the sub-device in the initial notification callback.

Implementation:

// ...

STATIC VOID __dev_hb_cb(CONST CHAR_T *dev_id)
{
    OPERATE_RET op_ret = OPRT_OK;
    TY_Z3_APS_FRAME_S frame = {0};
    USHORT_T atrr_buf[1] = { 0x0001 };

    strncpy(frame.id, dev_id, SIZEOF(frame.id));
    frame.profile_id = PROFILE_ID_HA;
    frame.cluster_id = ZCL_BASIC_CLUSTER_ID;
    frame.cmd_type = ZCL_CMD_TYPE_GLOBAL;
    frame.src_endpoint = 0x01;
    frame.dst_endpoint = 0xFF;
    frame.cmd_id = ZCL_READ_ATTRIBUTES_COMMAND_ID;

    frame.msg_length = SIZEOF(atrr_buf);
    frame.message = (UCHAR_T *)atrr_buf;

    op_ret = tuya_zigbee_send_data(&frame);
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_zigbee_send_data err: %d", op_ret);
        return;
    }
}

STATIC VOID __my_z3_dev_report(TY_Z3_APS_FRAME_S *frame)
{
    OPERATE_RET op_ret = OPRT_OK;
    DEV_DESC_IF_S *dev_if = NULL;

    if (frame->profile_id != PROFILE_ID_HA) {
        return;
    }

    dev_if = tuya_iot_get_dev_if(frame->id);
    if (!dev_if || !dev_if->bind) {
        PR_ERR("dev_id: %s is not bind", frame->id);
        tuya_zigbee_del_dev(frame->id);
        return;
    }

    op_ret = tuya_iot_fresh_dev_hb(frame->id);
    if (op_ret != OPRT_OK) {
        PR_WARN("tuya_iot_fresh_dev_hb err: %d", op_ret);
    }

    UCHAR_T uc_devid = (((dev_if->uddd) >> 8) & 0xFF);
    switch (uc_devid)
    {
        case TY_DEVID_HA_ONOF_LIGHT:
            __z3_onoff_light_report(frame);
            break;
        case TY_DEVID_HA_IAS_ZONE:
        case TY_DEVID_HA_TEMPERATURE_SENSOR:
        case TY_DEVID_HA_SMART_PLUG:   
        default:
            PR_WARN("devid: %d is not support", uc_devid);
            return;
    }
}

STATIC VOID __my_z3_dev_notify(VOID)
{
    OPERATE_RET op_ret = OPRT_OK;
    DEV_DESC_IF_S *dev_if = NULL;
    VOID *iter = NULL;

    dev_if = tuya_iot_dev_traversal(&iter);
    while (dev_if) {
        if (dev_if->tp == GP_DEV_ATH_1) {
            op_ret = tuya_iot_set_dev_hb_cfg(dev_if->id, 120, 3, FALSE);
            if (op_ret != OPRT_OK) {
                PR_WARN("tuya_iot_set_dev_hb_cfg err: %d", op_ret);
            }
        }
        dev_if = tuya_iot_dev_traversal(&iter);
    }
}

STATIC OPERATE_RET __user_init(VOID)
{
    OPERATE_RET rt = OPRT_OK;
    /* Zigbee sub-device management callbacks */
    TY_Z3_DEV_CBS_S z3_dev_cbs = {
        .join        = __my_z3_dev_join,
        .report      = __my_z3_dev_report,
        .notify      = __my_z3_dev_notify,
    };

    /* SDK sub-device management callbacks */
    TY_IOT_DEV_CBS_S dev_mgr_cbs = {
        .dev_bind_ifm  = __dev_bind_cb,
        .dev_hb        = __dev_hb_cb,
    };

    TY_IOT_DP_CBS_S dev_dp_cbs = {
        .obj   = __dev_cmd_obj_cb,
    };

    TUYA_CALL_ERR_RETURN(tuya_zigbee_custom_dev_mgr_init(&my_z3_devlist, &z3_dev_cbs));
    TUYA_CALL_ERR_RETURN(tuya_iot_reg_gw_mgr_cb(GP_DEV_ATH_1, &dev_mgr_cbs));
    TUYA_CALL_ERR_RETURN(tuya_iot_reg_dp_cb(DP_DEV, GP_DEV_ATH_1, &dev_dp_cbs));

    return OPRT_OK;
}

// ...

Device reset

There are two approaches to removing a third-party Zigbee device:

  • Use the mobile app.
    • Register the SDK sub-device management callback. In the removal callback and reset callback, remove the third-party device.
  • Use the physical reset button on the device.
    • Register the Zigbee sub-device management callback. In the removal callback, unbind the third-party device.

Implementation:

// ...

STATIC VOID __my_z3_dev_leave(CONST CHAR_T *dev_id)
{
    OPERATE_RET op_ret = OPRT_OK;

    op_ret = tuya_iot_gw_unbind_dev(dev_id);
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_iot_gw_unbind_dev err: %d", op_ret);
        return;
    }
}

STATIC VOID __dev_del_cb(CONST CHAR_T *dev_id, CONST GW_DELDEV_TYPE type)
{
    OPERATE_RET op_ret = OPRT_OK;

    op_ret = tuya_zigbee_del_dev(dev_id);
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_zigbee_del_dev err: %d", op_ret);
        return;
    }
}

STATIC VOID __dev_reset_cb(CONST CHAR_T *dev_id, DEV_RESET_TYPE_E type)
{
    OPERATE_RET op_ret = OPRT_OK;

    op_ret = tuya_zigbee_del_dev(dev_id);
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_zigbee_del_dev err: %d", op_ret);
        return;
    }
}

STATIC OPERATE_RET __user_init(VOID)
{
    OPERATE_RET rt = OPRT_OK;
    /* Zigbee sub-device management callbacks */
    TY_Z3_DEV_CBS_S z3_dev_cbs = {
        .join        = __my_z3_dev_join,
        .leave       = __my_z3_dev_leave,
        .report      = __my_z3_dev_report,
        .notify      = __my_z3_dev_notify,
    };

    /* SDK sub-device management callbacks */
    TY_IOT_DEV_CBS_S dev_mgr_cbs = {
        .dev_del       = __dev_del_cb,
        .dev_bind_ifm  = __dev_bind_cb,
        .dev_hb        = __dev_hb_cb,
        .dev_reset     = __dev_reset_cb,
    };

    TY_IOT_DP_CBS_S dev_dp_cbs = {
        .obj   = __dev_cmd_obj_cb,
    };

    TUYA_CALL_ERR_RETURN(tuya_zigbee_custom_dev_mgr_init(&my_z3_devlist, &z3_dev_cbs));
    TUYA_CALL_ERR_RETURN(tuya_iot_reg_gw_mgr_cb(GP_DEV_ATH_1, &dev_mgr_cbs));
    TUYA_CALL_ERR_RETURN(tuya_iot_reg_dp_cb(DP_DEV, GP_DEV_ATH_1, &dev_dp_cbs));

    return OPRT_OK;
}

// ...

Device update

The Zigbee OTA Cluster is used to update the Zigbee sub-device. The SDK enables firmware update download and storage and update transfer to sub-devices. The application can call the corresponding functions to perform OTA updates for sub-devices.

For more information about OTA update deployment, see OTA update.

To implement device update:

  • Register the SDK sub-device management callback. In the update notification callback, call the sub-device update function.
  • Register the Zigbee sub-device management callback. In the update result callback, implement your code. If the update is successful, report the new version number to the cloud. Otherwise, report a failed update.

Implementation:

// ...

STATIC VOID __dev_upgrade_cb(CONST CHAR_T *dev_id, CONST FW_UG_S *fw)
{
    OPERATE_RET op_ret = OPRT_OK;

    op_ret = tuya_zigbee_upgrade_dev(dev_id, fw);
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_zigbee_upgrade_dev err: %d", op_ret);
        return;
    }

    return;
}

STATIC VOID __my_z3_dev_upgrade_end(CONST CHAR_T *dev_id, INT_T rc, UCHAR_T version)
{
    CHAR_T sw_ver[SW_VER_LEN+1] = {0};

    if (rc != OPRT_OK) {
        tuya_iot_dev_upgd_result_report(dev_id, GP_DEV_ATH_1, 4);
        return;
    }

    CHAR_T iver1 = (CHAR_T)(version & 0x0F); 
    CHAR_T iver2 = (CHAR_T)((version >> 4) & 0x03);
    CHAR_T iver3 = (CHAR_T)((version >> 6) & 0x03);
    snprintf(sw_ver, SIZEOF(sw_ver), "%d.%d.%d", iver3, iver2, iver1);

    tuya_iot_gw_subdevice_update(dev_id, sw_ver);

    return;
}

STATIC OPERATE_RET __user_init(VOID)
{
    OPERATE_RET rt = OPRT_OK;
    /* Zigbee sub-device management callbacks */
    TY_Z3_DEV_CBS_S z3_dev_cbs = {
        .join        = __my_z3_dev_join,
        .leave       = __my_z3_dev_leave,
        .report      = __my_z3_dev_report,
        .notify      = __my_z3_dev_notify,
        .upgrade_end = __my_z3_dev_upgrade_end,
    };

    /* SDK sub-device management callbacks */
    TY_IOT_DEV_CBS_S dev_mgr_cbs = {
        .dev_del       = __dev_del_cb,
        .dev_bind_ifm  = __dev_bind_cb,
        .dev_hb        = __dev_hb_cb,
        .dev_reset     = __dev_reset_cb,
        .dev_upgrade   = __dev_upgrade_cb,
    };

    TY_IOT_DP_CBS_S dev_dp_cbs = {
        .obj   = __dev_cmd_obj_cb,
    };

    TUYA_CALL_ERR_RETURN(tuya_zigbee_custom_dev_mgr_init(&my_z3_devlist, &z3_dev_cbs));
    TUYA_CALL_ERR_RETURN(tuya_iot_reg_gw_mgr_cb(GP_DEV_ATH_1, &dev_mgr_cbs));
    TUYA_CALL_ERR_RETURN(tuya_iot_reg_dp_cb(DP_DEV, GP_DEV_ATH_1, &dev_dp_cbs));

    return OPRT_OK;
}

// ...

Sync status

After startup, the application reads the current status of the sub-device and syncs it to the cloud to make sure the device status shown on the app is up to date.

To implement status sync:

  • Register the Zigbee sub-device management callback. In the initial notification callback, traverse all third-party Zigbee devices and read their status.
  • In the data reporting callback, you implement heartbeat refresh, convert the ZCL data into the DP data, and report data to the cloud.

Implementation:

// ...

STATIC VOID __my_z3_dev_report(TY_Z3_APS_FRAME_S *frame)
{
    OPERATE_RET op_ret = OPRT_OK;
    DEV_DESC_IF_S *dev_if = NULL;

    if (frame->profile_id != PROFILE_ID_HA) {
        return;
    }

    dev_if = tuya_iot_get_dev_if(frame->id);
    if (!dev_if || !dev_if->bind) {
        PR_ERR("dev_id: %s is not bind", frame->id);
        tuya_zigbee_del_dev(frame->id);
        return;
    }

    op_ret = tuya_iot_fresh_dev_hb(frame->id);
    if (op_ret != OPRT_OK) {
        PR_WARN("tuya_iot_fresh_dev_hb err: %d", op_ret);
    }

    UCHAR_T uc_devid = (((dev_if->uddd) >> 8) & 0xFF);
    switch (uc_devid)
    {
        case TY_DEVID_HA_ONOF_LIGHT:
            __z3_onoff_light_report(frame);
            break;
        case TY_DEVID_HA_IAS_ZONE:
        case TY_DEVID_HA_TEMPERATURE_SENSOR:
        case TY_DEVID_HA_SMART_PLUG:   
        default:
            PR_WARN("devid: %d is not support", uc_devid);
            return;
    }
}

STATIC VOID __my_z3_dev_notify(VOID)
{
    OPERATE_RET op_ret = OPRT_OK;
    DEV_DESC_IF_S *dev_if = NULL;
    VOID *iter = NULL;

    dev_if = tuya_iot_dev_traversal(&iter);
    while (dev_if) {
        if ((dev_if->tp == GP_DEV_ATH_1) && dev_if->bind) {
            op_ret = tuya_iot_set_dev_hb_cfg(dev_if->id, 120, 3, FALSE);
            if (op_ret != OPRT_OK) {
                PR_WARN("tuya_iot_set_dev_hb_cfg err: %d", op_ret);
            }

            UCHAR_T uc_devid = (((dev_if->uddd) >> 8) & 0xFF);
            switch (uc_devid)
            {
                case TY_DEVID_HA_ONOF_LIGHT:
                    __z3_onoff_light_read_attr(dev_if->id);
                    break;
                case TY_DEVID_HA_IAS_ZONE:
                case TY_DEVID_HA_TEMPERATURE_SENSOR:
                case TY_DEVID_HA_SMART_PLUG:   
                default:
                    PR_WARN("devid: %d is not support", uc_devid);
                    break;
            }
        }
        dev_if = tuya_iot_dev_traversal(&iter);
    }
}

STATIC OPERATE_RET __user_init(VOID)
{
    OPERATE_RET rt = OPRT_OK;
    /* Zigbee sub-device management callbacks */
    TY_Z3_DEV_CBS_S z3_dev_cbs = {
        .join        = __my_z3_dev_join,
        .leave       = __my_z3_dev_leave,
        .report      = __my_z3_dev_report,
        .notify      = __my_z3_dev_notify,
    };

    /* SDK sub-device management callbacks */
    TY_IOT_DEV_CBS_S dev_mgr_cbs = {
        .dev_del       = __dev_del_cb,
        .dev_bind_ifm  = __dev_bind_cb,
        .dev_hb        = __dev_hb_cb,
        .dev_reset     = __dev_reset_cb,
    };

    TY_IOT_DP_CBS_S dev_dp_cbs = {
        .obj   = __dev_cmd_obj_cb,
    };

    TUYA_CALL_ERR_RETURN(tuya_zigbee_custom_dev_mgr_init(&my_z3_devlist, &z3_dev_cbs));
    TUYA_CALL_ERR_RETURN(tuya_iot_reg_gw_mgr_cb(GP_DEV_ATH_1, &dev_mgr_cbs));
    TUYA_CALL_ERR_RETURN(tuya_iot_reg_dp_cb(DP_DEV, GP_DEV_ATH_1, &dev_dp_cbs));

    return OPRT_OK;
}

// ...

Appendix

Full code for Zigbee basics

#include <unistd.h>

#include "uni_log.h"
#include "base_os_adapter.h"
#include "tuya_iot_base_api.h"
#include "tuya_iot_com_api.h"

#include "tuya_iot_sdk_api.h"
#include "tuya_iot_sdk_defs.h"

#include "tuya_zigbee_api.h"

#if defined(TY_BT_MOD) && (TY_BT_MOD == 1)
#include "tuya_os_adapt_bt.h"
#endif

#define PID       "fljmamufiym5fktz"                  // Replace it with your own PID.
#define UUID      "tuya461dbc63aeeb991f"              // Replace it with your own UUID.
#define AUTHKEY   "c8X4PR4wx1gMFaQlaZu5dfgVvVRwB8Ug"  // Replace it with your own AUTHKEY.

STATIC VOID __gw_reset_cb(GW_RESET_TYPE_E type)
{
    PR_DEBUG("gw reset callback, type: %d", type);

    if (GW_RESET_DATA_FACTORY != type) {
        exit(0);
    }

    return;
}

STATIC VOID __gw_upgrade_cb(CONST FW_UG_S *fw)
{
    PR_DEBUG("gw upgrade callback");

    if (fw == NULL) {
        PR_ERR("invalid param");
        return;
    }

    PR_DEBUG("        tp: %d", fw->tp);
    PR_DEBUG("    fw_url: %s", fw->fw_url);
    PR_DEBUG("    sw_ver: %s", fw->sw_ver);
    PR_DEBUG("   fw_hmac: %s", fw->fw_hmac);
    PR_DEBUG(" file_size: %u", fw->file_size);

    return;
}

STATIC VOID __gw_active_stat_cb(GW_STATUS_E status)
{
    PR_DEBUG("gw active stat callback, status: %d", status);

    return;
}

STATIC VOID __gw_reboot_cb(VOID)
{
    PR_DEBUG("gw reboot callback");

    exit(0);

    return;
}

STATIC VOID __nw_stat_cb(IN CONST SDK_NW_STAT_T stat)
{
    PR_DEBUG("network stat: %d", stat);

    return;
}

STATIC VOID __wired_stat_cb(IN CONST SDK_WIRED_NW_STAT_T stat)
{
    PR_DEBUG("wired stat: %d", stat);

    return;
}

int main(int argc, char **argv)
{
    OPERATE_RET rt = OPRT_OK;
    GW_PROD_INFO_S prod_info = {0};

    /* gw base callback */
    TY_GW_INFRA_CBS_S gw_cbs = {
        .gw_reset_cb       = __gw_reset_cb,
        .gw_upgrade_cb     = __gw_upgrade_cb,
        .gw_active_stat_cb = __gw_active_stat_cb,
        .gw_reboot_cb      = __gw_reboot_cb,
    };

#if defined(TY_BT_MOD) && (TY_BT_MOD == 1)
    tuya_os_adapt_reg_bt_intf();
#endif

    /* initiate os-layer service*/
    tuya_os_intf_init();

    /* initiate iot-layer service */
    TUYA_CALL_ERR_RETURN(tuya_iot_init("./"));

    /* set the logging level to debug */
    SET_PR_DEBUG_LEVEL(TY_LOG_LEVEL_DEBUG);

    PR_DEBUG("SDK INFO: %s", tuya_iot_get_sdk_info());

    /* set uuid and authkey */
    prod_info.uuid     = UUID;
    prod_info.auth_key = AUTHKEY;
    TUYA_CALL_ERR_RETURN(tuya_iot_set_gw_prod_info(&prod_info));

    /* pre-initiate sdk service */
    TUYA_CALL_ERR_RETURN(tuya_iot_sdk_pre_init(TRUE));  

    ty_cJSON *zb_cfg = ty_cJSON_CreateObject();
    if (zb_cfg == NULL) {
        return OPRT_CJSON_GET_ERR;
    }
    ty_cJSON_AddStringToObject(zb_cfg, "storage_path", "./");
    ty_cJSON_AddStringToObject(zb_cfg, "cache_path", "/tmp/");
    ty_cJSON_AddStringToObject(zb_cfg, "dev_name", "/dev/ttyS2");
    ty_cJSON_AddNumberToObject(zb_cfg, "cts", 1);
    ty_cJSON_AddNumberToObject(zb_cfg, "thread_mode", 1);

    /* initiate application service, more service in here */
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_init(zb_cfg));
    TUYA_CALL_ERR_RETURN(tuya_user_svc_init(&gw_cbs));

    /* initiate sdk service */
#if defined(GW_SUPPORT_WIRED_WIFI) && (GW_SUPPORT_WIRED_WIFI==1)
    TUYA_CALL_ERR_RETURN(tuya_iot_wired_wf_sdk_init(IOT_GW_NET_WIRED_WIFI, GWCM_OLD, WF_START_AP_ONLY, PID, USER_SW_VER, NULL, 0));
#elif defined(WIFI_GW) && (WIFI_GW==1)
    TUYA_CALL_ERR_RETURN(tuya_iot_wf_sdk_init(GWCM_OLD, WF_START_AP_ONLY, PID, USER_SW_VER, NULL, 0));
#else
    TUYA_CALL_ERR_RETURN(tuya_iot_sdk_init(PID, USER_SW_VER, NULL, 0));
#endif

    /* register net stat notification callback */
    TUYA_CALL_ERR_RETURN(tuya_iot_sdk_reg_netstat_cb(__nw_stat_cb, __wired_stat_cb, NULL));

    /* start application service, more service in here */
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_start(zb_cfg));
    TUYA_CALL_ERR_RETURN(tuya_user_svc_start(NULL));

    while (1) {
        sleep(10);
    }

    return 0;
}

Full code for Zigbee extension

#include <stdio.h>

#include "uni_log.h"
#include "base_os_adapter.h"
#include "tuya_hal_system.h"
#include "tuya_iot_base_api.h"
#include "tuya_iot_com_api.h"

#include "tuya_iot_sdk_api.h"
#include "tuya_iot_sdk_defs.h"

#include "tuya_zigbee_api.h"
#include "tuya_gw_subdev_api.h"

#if defined(TY_BT_MOD) && (TY_BT_MOD == 1)
#include "tuya_os_adapt_bt.h"
#endif

#define PID       "fljmamufiym5fktz"                  // Replace it with your own PID.
#define UUID      "tuya461dbc63aeeb991f"              // Replace it with your own UUID.
#define AUTHKEY   "c8X4PR4wx1gMFaQlaZu5dfgVvVRwB8Ug"  // Replace it with your own AUTHKEY.

STATIC VOID __gw_reset_cb(GW_RESET_TYPE_E type)
{
    PR_DEBUG("gw reset callback, type: %d", type);

    if (GW_RESET_DATA_FACTORY != type) {
        exit(0);
    }

    return;
}

STATIC VOID __gw_upgrade_cb(CONST FW_UG_S *fw)
{
    PR_DEBUG("gw upgrade callback");

    if (fw == NULL) {
        PR_ERR("invalid param");
        return;
    }

    PR_DEBUG("        tp: %d", fw->tp);
    PR_DEBUG("    fw_url: %s", fw->fw_url);
    PR_DEBUG("    sw_ver: %s", fw->sw_ver);
    PR_DEBUG("   fw_hmac: %s", fw->fw_hmac);
    PR_DEBUG(" file_size: %u", fw->file_size);

    return;
}

STATIC VOID __gw_active_stat_cb(GW_STATUS_E status)
{
    PR_DEBUG("gw active stat callback, status: %d", status);

    return;
}

STATIC VOID __gw_reboot_cb(VOID)
{
    PR_DEBUG("gw reboot callback");

    exit(0);

    return;
}

STATIC VOID __nw_stat_cb(IN CONST SDK_NW_STAT_T stat)
{
    PR_DEBUG("network stat: %d", stat);

    return;
}

STATIC VOID __wired_stat_cb(IN CONST SDK_WIRED_NW_STAT_T stat)
{
    PR_DEBUG("wired stat: %d", stat);

    return;
}

#define TY_TS_ONOF_LIGHT_ONE                    "TS0001"
#define TY_TS_ONOF_LIGHT_TWO                    "TS0002"
#define TY_TS_ONOF_LIGHT_THR                    "TS0003"

#define PROFILE_ID_HA                           0x0104
#define PROFILE_ID_LL                           0xC05E

#define ZCL_BASIC_CLUSTER_ID                    0x0000
#define ZCL_POWER_CONFIG_CLUSTER_ID             0x0001
#define ZCL_DEVICE_TEMP_CLUSTER_ID              0x0002
#define ZCL_IDENTIFY_CLUSTER_ID                 0x0003
#define ZCL_GROUPS_CLUSTER_ID                   0x0004
#define ZCL_SCENES_CLUSTER_ID                   0x0005
#define ZCL_ON_OFF_CLUSTER_ID                   0x0006

#define ZCL_CMD_TYPE_GLOBAL                     0x00
#define ZCL_CMD_TYPE_PRIVATE                    0x01

#define ZCL_FRAME_TYPE_UNICAST                  0x00
#define ZCL_FRAME_TYPE_MULTICAST                0x01
#define ZCL_FRAME_TYPE_BROADCAST                0x02

#define ZCL_OFF_COMMAND_ID                      0x00
#define ZCL_ON_COMMAND_ID                       0x01

#define ZCL_READ_ATTRIBUTES_COMMAND_ID          0x00
#define ZCL_READ_ATTRIBUTES_RESPONSE_COMMAND_ID 0x01
#define ZCL_REPORT_ATTRIBUTES_COMMAND_ID        0x0A
#define ZCL_READ_ATTRIBUTER_RESPONSE_HEADER     3 /* Attributer ID: 2 Bytes, Status: 1 Btye */
#define ZCL_REPORT_ATTRIBUTES_HEADER            2 /* Attributer ID: 2 Bytes */

#define DEVID_HA_IAS_ZONE                       0x0402    // 0x0402 -> 0x00  IAS Zone
#define DEVID_HA_TEMPERATURE_SENSOR             0x0302    // 0x0302 -> 0x01  Temperature Sensor
#define DEVID_HA_ONOF_LIGHT                     0x0100    // 0x0100 -> 0x02  On/off light
#define DEVID_HA_SMART_PLUG                     0x0051    // 0x0051 -> 0x03  Smart Plug

/* Custom DEVID, saved to UDDD */
#define TY_DEVID_HA_IAS_ZONE                    0x00      // 0x0402 -> 0x00  IAS Zone
#define TY_DEVID_HA_TEMPERATURE_SENSOR          0x01      // 0x0302 -> 0x01  Temperature Sensor
#define TY_DEVID_HA_ONOF_LIGHT                  0x02      // 0x0100 -> 0x02  On/off light
#define TY_DEVID_HA_SMART_PLUG                  0x03      // 0x0051 -> 0x03  Smart Plug

/* Custom TYPE, saved to UDDD */
#define TY_TYPE_PIR                             0x00      // DEVID_HA_IAS_ZONE
#define TY_TYPE_DOOR_SENSOR                     0x01      // DEVID_HA_IAS_ZONE
#define TY_TYPE_FIRE_SENSOR                     0x02      // DEVID_HA_IAS_ZONE
#define TY_TYPE_WATER_SENSOR                    0x03      // DEVID_HA_IAS_ZONE

#define TY_TYPE_ONOF_LIGHT_ONE                  0x00      // DEVID_HA_ONOF_LIGHT
#define TY_TYPE_ONOF_LIGHT_TWO                  0x01      // DEVID_HA_ONOF_LIGHT
#define TY_TYPE_ONOF_LIGHT_THREE                0x02      // DEVID_HA_ONOF_LIGHT

TY_Z3_DEV_S my_z3_dev[] = {
    { "TUYATEC-nPGIPl5D", "TS0001" },
    { "TUYATEC-1xAC7DHQ", "TS0002" },
};

TY_Z3_DEVLIST_S my_z3_devlist = {
    .devs = my_z3_dev,
    .dev_num = CNTSOF(my_z3_dev),
};

STATIC VOID __z3_onoff_light_write_attr(CONST TY_RECV_OBJ_DP_S *cmd)
{
    INT_T i= 0;
    OPERATE_RET op_ret = OPRT_OK;
    TY_Z3_APS_FRAME_S frame = {0};

    strncpy(frame.id, cmd->cid, SIZEOF(frame.id));
    frame.profile_id = PROFILE_ID_HA;
    frame.src_endpoint = 0x01;

    for (i = 0; i < cmd->dps_cnt; i++) {
        switch (cmd->dps[i].dpid) {
        case 1 ... 3:
            frame.dst_endpoint = cmd->dps[i].dpid;
            frame.cluster_id = ZCL_ON_OFF_CLUSTER_ID;
            frame.cmd_type = ZCL_CMD_TYPE_PRIVATE;
            frame.frame_type = ZCL_FRAME_TYPE_UNICAST;
            frame.msg_length = 0;
            if (cmd->dps[i].value.dp_bool) {
                frame.cmd_id = ZCL_ON_COMMAND_ID;
            } else {
                frame.cmd_id = ZCL_OFF_COMMAND_ID;
            }
            break;
        default:
            PR_WARN("dev_id: %s, dpid: %d is not supported", cmd->cid, cmd->dps[i].dpid);
            return;
        }

        op_ret = tuya_zigbee_send_data(&frame);
        if (op_ret != OPRT_OK) {
            PR_WARN("tuya_zigbee_send_data err: %d", op_ret);
        }
    }
}

STATIC VOID __z3_onoff_light_read_attr(CONST CHAR_T *dev_id)
{
    OPERATE_RET op_ret = OPRT_OK;
    TY_Z3_APS_FRAME_S frame = {0};
    USHORT_T atrr_buf[5] = { 0x0000, 0x4001, 0x4002, 0x8001, 0x5000 };

    strncpy(frame.id, dev_id, SIZEOF(frame.id));
    frame.profile_id = PROFILE_ID_HA;
    frame.cluster_id = ZCL_ON_OFF_CLUSTER_ID;
    frame.cmd_type = ZCL_CMD_TYPE_GLOBAL;
    frame.src_endpoint = 0x01;
    frame.dst_endpoint = 0xFF;
    frame.cmd_id = ZCL_READ_ATTRIBUTES_COMMAND_ID;

    frame.msg_length = SIZEOF(atrr_buf);
    frame.message = (UCHAR_T *)atrr_buf;

    op_ret = tuya_zigbee_send_data(&frame);
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_zigbee_send_data err: %d", op_ret);
        return;
    }
}

STATIC VOID __z3_onoff_light_report(TY_Z3_APS_FRAME_S *frame)
{
    OPERATE_RET op_ret = OPRT_OK;
    TY_OBJ_DP_S dp_data = {0};

    if (frame->cluster_id != ZCL_ON_OFF_CLUSTER_ID) {
        return;
    }

    dp_data.dpid = frame->src_endpoint;
    dp_data.type = PROP_BOOL;

    if (frame->cmd_id == ZCL_REPORT_ATTRIBUTES_COMMAND_ID) {
        dp_data.value.dp_bool = frame->message[ZCL_REPORT_ATTRIBUTES_HEADER+1];
    } else if (frame->cmd_id == ZCL_READ_ATTRIBUTES_RESPONSE_COMMAND_ID) {
        dp_data.value.dp_bool = frame->message[ZCL_READ_ATTRIBUTER_RESPONSE_HEADER+1];
    } else {
        return;
    }

    op_ret = dev_report_dp_json_async(frame->id, &dp_data, 1);
    if (op_ret != OPRT_OK) {
        PR_ERR("dev_report_dp_json_async err: %d", op_ret);
        return;
    }
}

STATIC VOID __my_z3_dev_report(TY_Z3_APS_FRAME_S *frame)
{
    OPERATE_RET op_ret = OPRT_OK;
    DEV_DESC_IF_S *dev_if = NULL;

    if (frame->profile_id != PROFILE_ID_HA) {
        return;
    }

    dev_if = tuya_iot_get_dev_if(frame->id);
    if (!dev_if || !dev_if->bind) {
        PR_ERR("dev_id: %s is not bind", frame->id);
        tuya_zigbee_del_dev(frame->id);
        return;
    }

    op_ret = tuya_iot_fresh_dev_hb(frame->id);
    if (op_ret != OPRT_OK) {
        PR_WARN("tuya_iot_fresh_dev_hb err: %d", op_ret);
    }

    UCHAR_T uc_devid = (((dev_if->uddd) >> 8) & 0xFF);
    switch (uc_devid)
    {
        case TY_DEVID_HA_ONOF_LIGHT:
            __z3_onoff_light_report(frame);
            break;
        case TY_DEVID_HA_IAS_ZONE:
        case TY_DEVID_HA_TEMPERATURE_SENSOR:
        case TY_DEVID_HA_SMART_PLUG:   
        default:
            PR_WARN("devid: %d is not support", uc_devid);
            return;
    }
}

STATIC VOID __my_z3_dev_notify(VOID)
{
    OPERATE_RET op_ret = OPRT_OK;
    DEV_DESC_IF_S *dev_if = NULL;
    VOID *iter = NULL;

    dev_if = tuya_iot_dev_traversal(&iter);
    while (dev_if) {
        if ((dev_if->tp == GP_DEV_ATH_1) && dev_if->bind) {
            op_ret = tuya_iot_set_dev_hb_cfg(dev_if->id, 120, 3, FALSE);
            if (op_ret != OPRT_OK) {
                PR_WARN("tuya_iot_set_dev_hb_cfg err: %d", op_ret);
            }

            UCHAR_T uc_devid = (((dev_if->uddd) >> 8) & 0xFF);
            switch (uc_devid)
            {
                case TY_DEVID_HA_ONOF_LIGHT:
                    __z3_onoff_light_read_attr(dev_if->id);
                    break;
                case TY_DEVID_HA_IAS_ZONE:
                case TY_DEVID_HA_TEMPERATURE_SENSOR:
                case TY_DEVID_HA_SMART_PLUG:   
                default:
                    PR_WARN("devid: %d is not support", uc_devid);
                    break;
            }
        }
        dev_if = tuya_iot_dev_traversal(&iter);
    }
}

STATIC VOID __my_z3_dev_upgrade_end(CONST CHAR_T *dev_id, INT_T rc, UCHAR_T version)
{
    CHAR_T sw_ver[SW_VER_LEN+1] = {0};

    if (rc != OPRT_OK) {
        tuya_iot_dev_upgd_result_report(dev_id, GP_DEV_ATH_1, 4);
        return;
    }

    CHAR_T iver1 = (CHAR_T)(version & 0x0F); 
    CHAR_T iver2 = (CHAR_T)((version >> 4) & 0x03);
    CHAR_T iver3 = (CHAR_T)((version >> 6) & 0x03);
    snprintf(sw_ver, SIZEOF(sw_ver), "%d.%d.%d", iver3, iver2, iver1);

    tuya_iot_gw_subdevice_update(dev_id, sw_ver);

    return;
}

STATIC VOID __my_z3_dev_join(TY_Z3_DESC_S *dev)
{
    OPERATE_RET op_ret = OPRT_OK;
    CHAR_T pid[PRODUCT_KEY_LEN+1]={0};
    CHAR_T sw_ver[SW_VER_LEN+1] = {0};
    USER_DEV_DTL_DEF_T uddd = 0;

    /**
     * The sample code is for demonstration only. You need to:
     *   a) Parse device information, extract the necessary information, and save it to the uddd.
     *   b) Call the device binding function to bind the device with the cloud by PID.
     */

    if (dev->profile_id[0] != PROFILE_ID_HA) {
        return;
    }

    CHAR_T iver1 = (CHAR_T)(dev->version & 0x0F); 
    CHAR_T iver2 = (CHAR_T)((dev->version >> 4) & 0x03);
    CHAR_T iver3 = (CHAR_T)((dev->version >> 6) & 0x03);
    snprintf(sw_ver, SIZEOF(sw_ver), "%d.%d.%d", iver3, iver2, iver1);

    switch (dev->device_id[0]) {
        case DEVID_HA_ONOF_LIGHT:
            uddd |= (TY_DEVID_HA_ONOF_LIGHT << 8);
            if (0 == strncasecmp(dev->model_id, TY_TS_ONOF_LIGHT_ONE, strlen(TY_TS_ONOF_LIGHT_ONE))) {
                uddd |= (TY_TYPE_ONOF_LIGHT_ONE & 0xFF);
                strncpy(pid, "ckj1pnvy", SIZEOF(pid));
            } else if (0 == strncasecmp(dev->model_id, TY_TS_ONOF_LIGHT_TWO, strlen(TY_TS_ONOF_LIGHT_TWO))) {
                uddd |= (TY_TYPE_ONOF_LIGHT_TWO & 0xFF);
                strncpy(pid, "tr4dshuh", SIZEOF(pid));
            } else {
                PR_WARN("model_id: %s is not supported", dev->model_id);
                return;
            }
            break;
        case DEVID_HA_IAS_ZONE:
        case DEVID_HA_TEMPERATURE_SENSOR:
        case DEVID_HA_SMART_PLUG:
        default:
            PR_WARN("devid: %d is not supported", dev->device_id[0]);
            return;
    }

    op_ret = tuya_iot_gw_bind_dev(GP_DEV_ATH_1, uddd, dev->id, pid, sw_ver);
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_iot_gw_bind_dev err: %d", op_ret);
        return;
    }
}

STATIC VOID __my_z3_dev_leave(CONST CHAR_T *dev_id)
{
    OPERATE_RET op_ret = OPRT_OK;

    op_ret = tuya_iot_gw_unbind_dev(dev_id);
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_iot_gw_unbind_dev err: %d", op_ret);
        return;
    }
}

STATIC VOID __dev_del_cb(CONST CHAR_T *dev_id, CONST GW_DELDEV_TYPE type)
{
    OPERATE_RET op_ret = OPRT_OK;

    op_ret = tuya_zigbee_del_dev(dev_id);
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_zigbee_del_dev err: %d", op_ret);
        return;
    }
}

STATIC VOID __dev_reset_cb(CONST CHAR_T *dev_id, DEV_RESET_TYPE_E type)
{
    OPERATE_RET op_ret = OPRT_OK;

    op_ret = tuya_zigbee_del_dev(dev_id);
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_zigbee_del_dev err: %d", op_ret);
        return;
    }
}

STATIC VOID __dev_upgrade_cb(CONST CHAR_T *dev_id, CONST FW_UG_S *fw)
{
    OPERATE_RET op_ret = OPRT_OK;

    op_ret = tuya_zigbee_upgrade_dev(dev_id, fw);
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_zigbee_upgrade_dev err: %d", op_ret);
        return;
    }

    return;
}

STATIC VOID __dev_bind_cb(CONST CHAR_T *dev_id, CONST OPERATE_RET result)
{
    OPERATE_RET op_ret = OPRT_OK;
    DEV_DESC_IF_S *dev_if = NULL;

    /**
     * The sample code is for demonstration only. You need to:
     *   a) Encapsulate the ZCL Read Attributes command based on device binding status and send it to the sub-device.
     *   b) Configure the heartbeat timeout.
     */

    if (result != OPRT_OK) {
        PR_ERR("dev_id: %s bind err", dev_id);
        tuya_zigbee_del_dev(dev_id);
        return;
    }

    op_ret = tuya_iot_set_dev_hb_cfg(dev_id, 120, 3, FALSE);
    if (op_ret != OPRT_OK) {
        PR_WARN("tuya_iot_set_dev_hb_cfg err: %d", op_ret);
    }

    op_ret = tuya_iot_fresh_dev_hb(dev_id);
    if (op_ret != OPRT_OK) {
        PR_WARN("tuya_iot_fresh_dev_hb err: %d", op_ret);
    }

    dev_if = tuya_iot_get_dev_if(dev_id);
    if (!dev_if || !dev_if->bind) {
        PR_ERR("dev_id: %s is not bind", dev_id);
        tuya_zigbee_del_dev(dev_id);
        return;
    }

    UCHAR_T uc_devid = (((dev_if->uddd) >> 8) & 0xFF);
    switch (uc_devid)
    {
        case TY_DEVID_HA_ONOF_LIGHT:
            __z3_onoff_light_read_attr(dev_id);
            break;
        case TY_DEVID_HA_IAS_ZONE:
        case TY_DEVID_HA_TEMPERATURE_SENSOR:
        case TY_DEVID_HA_SMART_PLUG:   
        default:
            PR_WARN("devid: %d is not support", uc_devid);
            return;
    }
}

STATIC VOID __dev_hb_cb(CONST CHAR_T *dev_id)
{
    OPERATE_RET op_ret = OPRT_OK;
    TY_Z3_APS_FRAME_S frame = {0};
    USHORT_T atrr_buf[1] = { 0x0001 };

    strncpy(frame.id, dev_id, SIZEOF(frame.id));
    frame.profile_id = PROFILE_ID_HA;
    frame.cluster_id = ZCL_BASIC_CLUSTER_ID;
    frame.cmd_type = ZCL_CMD_TYPE_GLOBAL;
    frame.src_endpoint = 0x01;
    frame.dst_endpoint = 0xFF;
    frame.cmd_id = ZCL_READ_ATTRIBUTES_COMMAND_ID;

    frame.msg_length = SIZEOF(atrr_buf);
    frame.message = (UCHAR_T *)atrr_buf;

    op_ret = tuya_zigbee_send_data(&frame);
    if (op_ret != OPRT_OK) {
        PR_ERR("tuya_zigbee_send_data err: %d", op_ret);
        return;
    }
}

STATIC VOID __dev_cmd_obj_cb(CONST TY_RECV_OBJ_DP_S *cmd)
{
    DEV_DESC_IF_S *dev_if = NULL;

    /**
     * The sample code is for demonstration only. You need to:
     *   a) Get the device's UDDD and map it to the device type.
     *   b) Convert the DP data to the ZCL data by UDDD and send it to the sub-device.
     */

    dev_if = tuya_iot_get_dev_if(cmd->cid);
    if (!dev_if || !dev_if->bind) {
        PR_ERR("dev_id: %s is not bind", cmd->cid);
        return;
    }

    UCHAR_T uc_devid = (((dev_if->uddd) >> 8) & 0xFF);
    switch (uc_devid)
    {
        case TY_DEVID_HA_ONOF_LIGHT:
            __z3_onoff_light_write_attr(cmd);
            break;
        case TY_DEVID_HA_IAS_ZONE:
        case TY_DEVID_HA_TEMPERATURE_SENSOR:
        case TY_DEVID_HA_SMART_PLUG:   
        default:
            PR_WARN("devid: %d is not support", uc_devid);
            return;
    }
}

STATIC OPERATE_RET __user_init(VOID)
{
    OPERATE_RET rt = OPRT_OK;
    /* Zigbee sub-device management callbacks */
    TY_Z3_DEV_CBS_S z3_dev_cbs = {
        .join        = __my_z3_dev_join,
        .leave       = __my_z3_dev_leave,
        .report      = __my_z3_dev_report,
        .notify      = __my_z3_dev_notify,
        .upgrade_end = __my_z3_dev_upgrade_end,
    };

    /* SDK sub-device management callbacks */
    TY_IOT_DEV_CBS_S dev_mgr_cbs = {
        .dev_del       = __dev_del_cb,
        .dev_bind_ifm  = __dev_bind_cb,
        .dev_hb        = __dev_hb_cb,
        .dev_reset     = __dev_reset_cb,
        .dev_upgrade   = __dev_upgrade_cb,
    };

    TY_IOT_DP_CBS_S dev_dp_cbs = {
        .obj   = __dev_cmd_obj_cb,
    };

    TUYA_CALL_ERR_RETURN(tuya_zigbee_custom_dev_mgr_init(&my_z3_devlist, &z3_dev_cbs));
    TUYA_CALL_ERR_RETURN(tuya_iot_reg_gw_mgr_cb(GP_DEV_ATH_1, &dev_mgr_cbs));
    TUYA_CALL_ERR_RETURN(tuya_iot_reg_dp_cb(DP_DEV, GP_DEV_ATH_1, &dev_dp_cbs));

    return OPRT_OK;
}

int main(int argc, char **argv)
{
    OPERATE_RET rt = OPRT_OK;
    GW_PROD_INFO_S prod_info = {0};

    /* gw base callback */
    TY_GW_INFRA_CBS_S gw_cbs = {
        .gw_reset_cb       = __gw_reset_cb,
        .gw_upgrade_cb     = __gw_upgrade_cb,
        .gw_active_stat_cb = __gw_active_stat_cb,
        .gw_reboot_cb      = __gw_reboot_cb,
    };

#if defined(TY_BT_MOD) && (TY_BT_MOD == 1)
    tuya_os_adapt_reg_bt_intf();
#endif

    /* initiate os-layer service*/
    tuya_os_intf_init();

    /* initiate iot-layer service */
    TUYA_CALL_ERR_RETURN(tuya_iot_init("./"));

    /* set the logging level to debug */
    SET_PR_DEBUG_LEVEL(TY_LOG_LEVEL_DEBUG);

    PR_DEBUG("SDK INFO: %s", tuya_iot_get_sdk_info());

    /* set uuid and authkey */
    prod_info.uuid     = UUID;
    prod_info.auth_key = AUTHKEY;
    TUYA_CALL_ERR_RETURN(tuya_iot_set_gw_prod_info(&prod_info));

    /* pre-initiate sdk service */
    TUYA_CALL_ERR_RETURN(tuya_iot_sdk_pre_init(TRUE));  

    ty_cJSON *zb_cfg = ty_cJSON_CreateObject();
    if (zb_cfg == NULL) {
        return OPRT_CJSON_GET_ERR;
    }
    ty_cJSON_AddStringToObject(zb_cfg, "storage_path", "./");
    ty_cJSON_AddStringToObject(zb_cfg, "cache_path", "/tmp/");
    ty_cJSON_AddStringToObject(zb_cfg, "dev_name", "/dev/ttyS2");
    ty_cJSON_AddNumberToObject(zb_cfg, "cts", 1);
    ty_cJSON_AddNumberToObject(zb_cfg, "thread_mode", 1);

    /* initiate application service, more service in here */
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_init(zb_cfg));
    TUYA_CALL_ERR_RETURN(tuya_user_svc_init(&gw_cbs));
    TUYA_CALL_ERR_RETURN(__user_init());

    /* initiate sdk service */
#if defined(GW_SUPPORT_WIRED_WIFI) && (GW_SUPPORT_WIRED_WIFI==1)
    TUYA_CALL_ERR_RETURN(tuya_iot_wired_wf_sdk_init(IOT_GW_NET_WIRED_WIFI, GWCM_OLD, WF_START_AP_ONLY, PID, USER_SW_VER, NULL, 0));
#elif defined(WIFI_GW) && (WIFI_GW==1)
    TUYA_CALL_ERR_RETURN(tuya_iot_wf_sdk_init(GWCM_OLD, WF_START_AP_ONLY, PID, USER_SW_VER, NULL, 0));
#else
    TUYA_CALL_ERR_RETURN(tuya_iot_sdk_init(PID, USER_SW_VER, NULL, 0));
#endif

    /* register net stat notification callback */
    TUYA_CALL_ERR_RETURN(tuya_iot_sdk_reg_netstat_cb(__nw_stat_cb, __wired_stat_cb, NULL));

    /* start application service, more service in here */
    TUYA_CALL_ERR_RETURN(tuya_zigbee_svc_start(zb_cfg));
    TUYA_CALL_ERR_RETURN(tuya_user_svc_start(NULL));

    tuya_user_svc_start(NULL);

    while (1) {
        tuya_hal_system_sleep(10*1000);
    }

    return 0;
}