I have an "Aqara Wireless Mini Switch" ( https://www.amazon.com/gp/product/B07D19YXND ) which identifies as a Xiaomi WXKG11LM. This button is designed to send semi-nonstandard commands to a supported ZC. It advertises a Multistate Input cluster, and when you press the button, it sends an unsolicited ZB_ZCL_CMD_REPORT_ATTRIB to the ZC. Compatible ZCs know that this means the user pressed the button, and they can be programmed to respond in different ways.
I would like to make my nRF52840 emulate the behavior of the Xiaomi switch. I have a working prototype, however the code is pretty hacky. I'd like to know if there is a better way of doing this. I'm worried that my implementation depends on too many internal ZBOSS details and will break easily if ZBOSS modifies their headers in the future.
The first hack is because I could not find any code in ZBOSS that implements the Multistate Input cluster:
/* Multistate input endpoint */
#define ZB_ZCL_CLUSTER_ID_MULTI_INPUT_SERVER_ROLE_INIT multistate_input_init_server
#define ZB_ZCL_CLUSTER_ID_MULTI_INPUT_CLIENT_ROLE_INIT multistate_input_init_client
zb_ret_t check_value_multistate_server(zb_uint16_t attr_id, zb_uint8_t endpoint, zb_uint8_t *value)
{
LOG_ERR("%s(0x%08x): not implemented", __func__, attr_id);
return RET_OK;
}
static zb_bool_t process_multistate_srv(zb_uint8_t param)
{
return ZB_FALSE;
}
void multistate_input_init_server()
{
zb_zcl_add_cluster_handlers(ZB_ZCL_CLUSTER_ID_MULTI_INPUT,
ZB_ZCL_CLUSTER_SERVER_ROLE,
check_value_multistate_server,
(zb_zcl_cluster_write_attr_hook_t)NULL,
process_multistate_srv);
}
void multistate_input_init_client()
{
k_oops();
}
struct multistate_attr_values {
zb_uint16_t number_of_states;
zb_bool_t out_of_service;
zb_uint16_t present_value;
zb_uint8_t status_flags;
};
static struct multistate_attr_values multistate_attr_values = {
.number_of_states = 16,
};
// ZBOSS supports the binary input cluster but not multistate input
// Reuse the binary input defs when they're helpful. Define the rest of
// the attributes here:
#define ZB_SET_ATTR_DESCR_WITH_MULTISTATE_INPUT_OUT_OF_SERVICE_ID \
ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_BINARY_INPUT_OUT_OF_SERVICE_ID
#define ZB_SET_ATTR_DESCR_WITH_MULTISTATE_INPUT_STATUS_FLAG_ID \
ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_BINARY_INPUT_STATUS_FLAG_ID
#define MULTISTATE_INPUT_NUMBER_OF_STATES_ID 0x004a
#define ZB_SET_ATTR_DESCR_WITH_MULTISTATE_INPUT_NUMBER_OF_STATES_ID(data_ptr) \
{ \
MULTISTATE_INPUT_NUMBER_OF_STATES_ID, \
ZB_ZCL_ATTR_TYPE_U16, \
ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_WRITE_OPTIONAL | ZB_ZCL_ATTR_ACCESS_REPORTING, \
(void*) data_ptr \
}
#define MULTISTATE_INPUT_PRESENT_VALUE_ID 0x0055
#define ZB_SET_ATTR_DESCR_WITH_MULTISTATE_INPUT_PRESENT_VALUE_ID(data_ptr) \
{ \
MULTISTATE_INPUT_PRESENT_VALUE_ID, \
ZB_ZCL_ATTR_TYPE_U16, \
ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_WRITE_OPTIONAL | ZB_ZCL_ATTR_ACCESS_REPORTING, \
(void*) data_ptr \
}
ZB_ZCL_START_DECLARE_ATTRIB_LIST(multistate_attr_list)
ZB_ZCL_SET_ATTR_DESC(MULTISTATE_INPUT_NUMBER_OF_STATES_ID, &multistate_attr_values.number_of_states)
ZB_ZCL_SET_ATTR_DESC(MULTISTATE_INPUT_OUT_OF_SERVICE_ID, &multistate_attr_values.out_of_service)
ZB_ZCL_SET_ATTR_DESC(MULTISTATE_INPUT_PRESENT_VALUE_ID, &multistate_attr_values.present_value)
ZB_ZCL_SET_ATTR_DESC(MULTISTATE_INPUT_STATUS_FLAG_ID, &multistate_attr_values.status_flags)
ZB_ZCL_FINISH_DECLARE_ATTRIB_LIST;
zb_zcl_cluster_desc_t multistate_input_cluster[] =
{
ZB_ZCL_CLUSTER_DESC(ZB_ZCL_CLUSTER_ID_MULTI_INPUT,
ZB_ZCL_ARRAY_SIZE(multistate_attr_list, zb_zcl_attr_t), /* attr_count */
multistate_attr_list, /* attr_desc_list */
ZB_ZCL_CLUSTER_SERVER_ROLE, /* cluster_role_mask */
ZB_ZCL_MANUF_CODE_INVALID /* manuf_code */
),
};
ZB_DECLARE_SIMPLE_DESC(2, 0);
ZB_AF_SIMPLE_DESC_TYPE(2, 0) simple_desc_multistate_ep =
{
MULTI_INPUT_ENDPOINT, /* endpoint */
ZB_AF_HA_PROFILE_ID, /* app_profile_id */
ZB_HA_TEST_DEVICE_ID, /* app_device_id */
0, /* app_device_version */
0, /* reserved */
1, /* app_input_cluster_count */
0, /* app_output_cluster_count */
{ /* app_cluster_list[] */
ZB_ZCL_CLUSTER_ID_MULTI_INPUT,
}
};
ZB_AF_DECLARE_ENDPOINT_DESC(multistate_ep,
MULTI_INPUT_ENDPOINT, /* ep_id */
ZB_AF_HA_PROFILE_ID, /* profile_id */
0, /* unused */
NULL, /* unused */
1, /* cluster_number */
multistate_input_cluster, /* cluster_list */
(zb_af_simple_desc_1_1_t*)&simple_desc_multistate_ep, /* simple_desc */
0, /* rep_count */
NULL, /* rep_ctx */
0, /* lev_ctrl_count */
NULL); /* lev_ctrl_ctx */
The second hack is because I could not figure out how to send an unsolicited ZB_ZCL_CMD_REPORT_ATTRIB packet without "decomposing" the ZBOSS macros and manually stitching the payload together, byte by byte:
static void send_multistate_report(zb_bufid_t bufid, zb_uint16_t idx_and_flags)
{
// Send a single multistate report to the ZC on initial button press
// This can be used to trigger events that can't be done with a
// standard binding. Intended to mimic the behavior of Xiaomi WXKG11LM
if ((idx_and_flags & FLAG_MASK) != 0) {
zb_buf_free(bufid);
return;
}
zb_uint16_t short_addr = 0;
// adapted from ZB_ZCL_CMD_ON_OFF_OFF_WITH_EFFECT_ID
zb_uint8_t *ptr = ZB_ZCL_START_PACKET_REQ(bufid)
// expanded from ZB_ZCL_CONSTRUCT_SPECIFIC_COMMAND_REQ_FRAME_CONTROL
// but with different flags set
ZB_ZCL_CONSTRUCT_FRAME_CONTROL(
ZB_ZCL_FRAME_TYPE_COMMON,
ZB_ZCL_NOT_MANUFACTURER_SPECIFIC,
ZB_ZCL_FRAME_DIRECTION_TO_CLI,
true), /* dis_default_resp */
0, /* no manuf_code */
ZB_ZCL_CONSTRUCT_COMMAND_HEADER_REQ(ptr, ZB_ZCL_GET_SEQ_NUM(),
ZB_ZCL_CMD_REPORT_ATTRIB);
ZB_ZCL_PACKET_PUT_DATA16_VAL(ptr, MULTISTATE_INPUT_PRESENT_VALUE_ID);
ZB_ZCL_PACKET_PUT_DATA8(ptr, ZB_ZCL_ATTR_TYPE_U16);
ZB_ZCL_PACKET_PUT_DATA16_VAL(ptr, idx_and_flags);
ZB_ZCL_FINISH_PACKET(bufid, ptr)
ZB_ZCL_SEND_COMMAND_SHORT(bufid,
short_addr, /* addr */
ZB_APS_ADDR_MODE_16_ENDP_PRESENT, /* dst_addr_mode */
1, /* dst_ep */
MULTI_INPUT_ENDPOINT, /* ep */
ZB_AF_HA_PROFILE_ID, /* prof_id */
ZB_ZCL_CLUSTER_ID_MULTI_INPUT, /* cluster */
NULL /* cb */);
}

