Implementing a zigbee endpoint callback handler

I'm stuck trying to add a callback handler to a simple HA ON OFF SWITCH application.

The application is running on the nrf52840 DK. The coordinator is Zigbee2MQTT (Z2M). The SDK is connect SDK 1.9.1

Here's my DK code:

/*
 * Copyright (c) 2021 Nordic Semiconductor ASA
 *
 * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
 */

/** @file
 *
 * @brief Zigbee application template.
 */

#include <zephyr.h>
#include <logging/log.h>
#include <dk_buttons_and_leds.h>

#include <zboss_api.h>
#include <zigbee/zigbee_error_handler.h>
#include <zigbee/zigbee_app_utils.h>
#include <zb_nrf_platform.h>

#include "zb_mem_config_med.h"
#include "zboss_api_addons.h"

// Endpoint IDs. Don't use CONFIG_ZIGBEE_FOTA_ENDPOINT, which is by default set to 10.

#define HA_ON_OFF_SWITCH_ENDPOINT		1
#define HA_WINDOW_COVERING_ENDPOINT		2


#define ERASE_PERSISTENT_CONFIG		ZB_TRUE
// Bitfield. bit set = select channel. Valid channels are 11 - 26.
// 0x07fff800U = Scan all channels, (1L << 11) | (1L << 12) = scan only channel 11 and 12.
#define IEEE_CHANNEL_MASK			(ZB_TRANSCEIVER_ALL_CHANNELS_MASK)


/* Version of the application software (1 byte). */
#define INIT_BASIC_APP_VERSION     ZB_ZCL_BASIC_APPLICATION_VERSION_DEFAULT_VALUE

/* Version of the implementation of the Zigbee stack (1 byte). */
#define INIT_BASIC_STACK_VERSION   ZB_ZCL_BASIC_STACK_VERSION_DEFAULT_VALUE

/* Version of the hardware of the device (1 byte). */
#define INIT_BASIC_HW_VERSION      ZB_ZCL_BASIC_HW_VERSION_DEFAULT_VALUE

/* Manufacturer name (32 bytes). */
#define INIT_BASIC_MANUF_NAME      "test_manufacturer"

/* Model number assigned by manufacturer (32-bytes long string). */
#define INIT_BASIC_MODEL_ID        "test_model_name"

/* First 8 bytes specify the date of manufacturer of the device
 * in ISO 8601 format (YYYYMMDD). The rest (8 bytes) are manufacturer specific.
 */
#define INIT_BASIC_DATE_CODE       "20220901"

/* Type of power sources available for the device.
 * For possible values see section 3.2.2.2.8 of ZCL specification.
 */
#define INIT_BASIC_POWER_SOURCE    ZB_ZCL_BASIC_POWER_SOURCE_DC_SOURCE

/* Describes the physical location of the device (16 bytes).
 * May be modified during commisioning process.
 */
#define INIT_BASIC_LOCATION_DESC   "Office desk"

/* Software Version (16 bytes). */
#define INIT_BASIC_SW_VER			"0.1"



/* LED indicating that device successfully joined Zigbee network. */
#define ZIGBEE_NETWORK_STATE_LED            DK_LED1

/* Button used to enter the Identify mode. */
#define IDENTIFY_MODE_BUTTON                DK_BTN1_MSK

LOG_MODULE_REGISTER(app, LOG_LEVEL_INF);

static void switch_send_on_off(zb_bufid_t bufid, zb_uint16_t cmd_id);

typedef struct
{
	zb_uint8_t type;
	zb_uint8_t actions;
} zb_zcl_on_off_switch_attrs_t;

/* Main application customizable context.
 * Stores all settings and static values.
 */
struct zb_device_ctx {
	zb_zcl_basic_attrs_ext_t		basic_attr;
	zb_zcl_identify_attrs_t			identify_attr;
    zb_zcl_scenes_attrs_t			scenes_attr;
    zb_zcl_groups_attrs_t			groups_attr;

	zb_zcl_on_off_switch_attrs_t	on_off_switch_attr;
};

/* Zigbee device application context storage. */
static struct zb_device_ctx dev_ctx;


// ATTRIB_LIST ------------------------------------------------------------------------------------
ZB_ZCL_DECLARE_BASIC_ATTRIB_LIST_EXT(basic_attr_list,
	&dev_ctx.basic_attr.zcl_version,
	&dev_ctx.basic_attr.app_version,
	&dev_ctx.basic_attr.stack_version,
	&dev_ctx.basic_attr.hw_version,
	dev_ctx.basic_attr.mf_name,
	dev_ctx.basic_attr.model_id,
	dev_ctx.basic_attr.date_code,
	&dev_ctx.basic_attr.power_source,
	dev_ctx.basic_attr.location_id,
	&dev_ctx.basic_attr.ph_env,
	dev_ctx.basic_attr.sw_ver
);

ZB_ZCL_DECLARE_IDENTIFY_ATTRIB_LIST(identify_attr_list,
	&dev_ctx.identify_attr.identify_time
);

ZB_ZCL_DECLARE_SCENES_ATTRIB_LIST(scenes_attr_list,
	&dev_ctx.scenes_attr.scene_count,
	&dev_ctx.scenes_attr.current_scene,
	&dev_ctx.scenes_attr.current_group,
	&dev_ctx.scenes_attr.scene_valid,
	&dev_ctx.scenes_attr.name_support
);

ZB_ZCL_DECLARE_GROUPS_ATTRIB_LIST(groups_attr_list, 
 	&dev_ctx.groups_attr.name_support
);

ZB_ZCL_DECLARE_ON_OFF_SWITCH_CONFIGURATION_ATTRIB_LIST(on_off_switch_attr_list,
	&dev_ctx.on_off_switch_attr.type,
	&dev_ctx.on_off_switch_attr.actions
);


// CLUSTER_LIST -----------------------------------------------------------------------------------
ZB_HA_DECLARE_ON_OFF_SWITCH_CLUSTER_LIST(on_off_switch_clusters,
	on_off_switch_attr_list,
	basic_attr_list,
	identify_attr_list
);

// EP ---------------------------------------------------------------------------------------------
ZB_HA_DECLARE_ON_OFF_SWITCH_EP(on_off_switch_ep,
	HA_ON_OFF_SWITCH_ENDPOINT,
	on_off_switch_clusters
);

// ZBOSS_DECLARE_DEVICE_CTX_x_EP ------------------------------------------------------------------
	ZBOSS_DECLARE_DEVICE_CTX_1_EP(app_device_ctx,
		on_off_switch_ep
	);



/**@brief Function for initializing all clusters attributes. */
static void app_clusters_attr_init(void)
{
	/* Basic cluster attributes data */
	dev_ctx.basic_attr.zcl_version   = ZB_ZCL_VERSION;
	dev_ctx.basic_attr.app_version   = INIT_BASIC_APP_VERSION;
	dev_ctx.basic_attr.stack_version = INIT_BASIC_STACK_VERSION;
	dev_ctx.basic_attr.hw_version    = INIT_BASIC_HW_VERSION;

	/* Use ZB_ZCL_SET_STRING_VAL to set strings, because the first byte
	 * should contain string length without trailing zero.
	 *
	 * For example "test" string wil be encoded as:
	 *   [(0x4), 't', 'e', 's', 't']
	 */
	ZB_ZCL_SET_STRING_VAL(
		dev_ctx.basic_attr.mf_name,
		INIT_BASIC_MANUF_NAME,
		ZB_ZCL_STRING_CONST_SIZE(INIT_BASIC_MANUF_NAME));

	ZB_ZCL_SET_STRING_VAL(
		dev_ctx.basic_attr.model_id,
		INIT_BASIC_MODEL_ID,
		ZB_ZCL_STRING_CONST_SIZE(INIT_BASIC_MODEL_ID));

	ZB_ZCL_SET_STRING_VAL(
		dev_ctx.basic_attr.date_code,
		INIT_BASIC_DATE_CODE,
		ZB_ZCL_STRING_CONST_SIZE(INIT_BASIC_DATE_CODE));

	dev_ctx.basic_attr.power_source = INIT_BASIC_POWER_SOURCE;

	ZB_ZCL_SET_STRING_VAL(
		dev_ctx.basic_attr.location_id,
		INIT_BASIC_LOCATION_DESC,
		ZB_ZCL_STRING_CONST_SIZE(INIT_BASIC_LOCATION_DESC));
	
	dev_ctx.basic_attr.ph_env = ZB_ZCL_BASIC_PHYSICAL_ENVIRONMENT_DEFAULT_VALUE;

	ZB_ZCL_SET_STRING_VAL(
		dev_ctx.basic_attr.sw_ver,
		INIT_BASIC_SW_VER,
		ZB_ZCL_STRING_CONST_SIZE(INIT_BASIC_SW_VER));


	/* Identify cluster attributes data. */
	dev_ctx.identify_attr.identify_time	= ZB_ZCL_IDENTIFY_IDENTIFY_TIME_DEFAULT_VALUE;

	// HA on/off switch
	dev_ctx.on_off_switch_attr.type		= ZB_ZCL_ON_OFF_SWITCH_CONFIGURATION_SWITCH_TYPE_TOGGLE;
	dev_ctx.on_off_switch_attr.actions	= ZB_ZCL_ON_OFF_SWITCH_CONFIGURATION_SWITCH_ACTIONS_DEFAULT_VALUE;
}

/**@brief Callback for button events.
 *
 * @param[in]   button_state  Bitmask containing buttons state.
 * @param[in]   has_changed   Bitmask containing buttons
 *                            that have changed their state.
 */
static void button_changed(uint32_t button_state, uint32_t has_changed)
{
static bool x = 0;

	// Has the IDENTIFY_MODE_BUTTON button been pressed?
	if (IDENTIFY_MODE_BUTTON & has_changed) 
	{
		if (IDENTIFY_MODE_BUTTON & button_state) 
		{
			/* Button changed its state to pressed */
			LOG_INF("btn is on");
		}
		else
		{
			/* Button changed its state to released */
			LOG_INF("btn is off");
			
			// Send a msg to Zigbee2Mqtt co-ord.
			zb_uint16_t cmd_id = (x == 0) ? ZB_ZCL_CMD_ON_OFF_ON_ID : ZB_ZCL_CMD_ON_OFF_OFF_ID;
			zb_ret_t zb_err_code = zb_buf_get_out_delayed_ext(switch_send_on_off, cmd_id, 0);
			ZB_ERROR_CHECK(zb_err_code);			

			x ^= 1;	// Next time this func is called, send the oppsite state.
		}
	}
}


/**@brief Function for sending ON/OFF requests to the network / co-ordinator.
 *
 * @param[in]   bufid    Non-zero reference to Zigbee stack buffer that will be
 *                       used to construct on/off request.
 * @param[in]   cmd_id   ZCL command id.
 */
static void switch_send_on_off(zb_bufid_t bufid, zb_uint16_t cmd_id)
{
	LOG_INF("Send ON/OFF command: %d", cmd_id);
	zb_uint16_t coordinator_address = 0x0000;
	dev_ctx.on_off_switch_attr.actions = (zb_bool_t)cmd_id;				

	ZB_ZCL_ON_OFF_SEND_REQ(bufid,
				coordinator_address,
				ZB_APS_ADDR_MODE_16_ENDP_PRESENT,
				HA_ON_OFF_SWITCH_ENDPOINT,
				HA_ON_OFF_SWITCH_ENDPOINT,
				ZB_AF_HA_PROFILE_ID,
				ZB_ZCL_ENABLE_DEFAULT_RESPONSE,
				cmd_id,
				NULL);
}


static zb_uint8_t on_off_switch_ep_handler(zb_bufid_t bufid)
{
//	zb_zcl_device_callback_param_t *device_cb_param = ZB_BUF_GET_PARAM(bufid, zb_zcl_device_callback_param_t);


	return ZB_FALSE;
}

/**@brief Function for initializing LEDs and Buttons. */
static void configure_gpio(void)
{
	int err;

	err = dk_buttons_init(button_changed);
	if (err) 
	{
		LOG_ERR("Cannot init buttons (err: %d)", err);
	}

	err = dk_leds_init();
	if (err) 
	{
		LOG_ERR("Cannot init LEDs (err: %d)", err);
	}
}

/**@brief Zigbee stack event handler.
 *
 * @param[in]   bufid   Reference to the Zigbee stack buffer
 *                      used to pass signal.
 */
void zboss_signal_handler(zb_bufid_t bufid)
{
	zb_zdo_app_signal_hdr_t *p_sg_p = NULL;
	zb_zdo_app_signal_type_t sig = zb_get_app_signal(bufid, &p_sg_p);
	zb_ret_t status = ZB_GET_APP_SIGNAL_STATUS(bufid);


	/* Update network status LED. */
	zigbee_led_status_update(bufid, ZIGBEE_NETWORK_STATE_LED);

	switch (sig) 
	{
		case ZB_BDB_SIGNAL_DEVICE_REBOOT:
		/* fall-through */

		case ZB_BDB_SIGNAL_STEERING:
			/* Call default signal handler. */
			ZB_ERROR_CHECK(zigbee_default_signal_handler(bufid));
			if (status == RET_OK) 
			{
			}
			break;

		default:
			/* Call default signal handler. */
			ZB_ERROR_CHECK(zigbee_default_signal_handler(bufid));
			break;
	}

	/* All callbacks should either reuse or free passed buffers.
	 * If bufid == 0, the buffer is invalid (not passed).
	 */
	if (bufid)
	{
		zb_buf_free(bufid);
	}
}


void main(void)
{
	LOG_INF("Starting Zigbee application");

	/* Initialize */
	configure_gpio();
	
	zb_set_network_ed_role(IEEE_CHANNEL_MASK);
	zigbee_erase_persistent_storage(ERASE_PERSISTENT_CONFIG);

	
	/* Register device context (endpoints). */
	memset( &dev_ctx, 0, sizeof(dev_ctx) );
	ZB_AF_REGISTER_DEVICE_CTX(&app_device_ctx);

	app_clusters_attr_init();

	ZB_AF_SET_ENDPOINT_HANDLER(HA_ON_OFF_SWITCH_ENDPOINT, on_off_switch_ep_handler);

	zigbee_enable();
	

	LOG_INF("Zigbee application started");
}

Problem 1:

Z2M allows the DK device to join.  During Z2M initialisation, Z2M attempts to read data from the DK, but results in an error. I think this is what the EP callback should be handling, but I cannot find any information of how to implement the EP handler.

Problem 2:

When a button is pressed on the DK, the ON OFF state is sent to Z2M, Z2M logs show this:

Debug 2022-09-06 13:44:42
Received Zigbee message from '0xf4ce367b62161a91', type 'commandOff', cluster 'genOnOff', data '{}' from endpoint 1 with groupID 0
Debug 2022-09-06 13:44:42
No converter available for 'test_manuf' with cluster 'genOnOff' and type 'commandOff' and data '{}'

Z2M custom device file (placed in a .js file next to the configuration.yaml file) :

const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const tz = require('zigbee-herdsman-converters/converters/toZigbee');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const extend = require('zigbee-herdsman-converters/lib/extend');
const e = exposes.presets;
const ea = exposes.access;

const definition = {
    zigbeeModel: ['test_model_name'],
    model: 'test_manuf',
    vendor: 'test_vendor',
    description: 'nRF zigbee test device',
    fromZigbee: [fz.on_off, fz.ignore_basic_report],
//    fromZigbee: [fz.on_off],
    toZigbee: [tz.on_off], // Should be empty, unless device can be controlled (e.g. lights, switches).
    exposes: [e.switch()], // Defines what this device exposes, used for e.g. Home Assistant discovery and in the frontend

    configure: async (device, coordinatorEndpoint, logger) => {
        const endpoint = device.getEndpoint(1);
        await reporting.bind(endpoint, coordinatorEndpoint, ['genOnOff']);
        await reporting.onOff(endpoint);
    },
};

module.exports = definition;

Related