How to add a custom attribute to your Z-Stack application

By claudio

Have you ever got in touch with ZigBee products? Now they are everywhere. You find them as small sensors in buildings or as wall switches where you don’t want to install cables into the wall. ZigBee is a quite versatile wireless protocol. It is based upon IEEE 802.15.4.

The ZigBee Aliance has descibed several different applications, where you can use ZigBee. These functionalities are grouped into clusters. Each cluster have associated commands and attributes.

For example: a light bulb or a switch would rely on the OnOff ZigBeeCluster. This cluster defines a On, Off and a Toggle command. The corresponding attribute would be a OnOff attribute which represents the actual state of the switch or the light bulb.

There are many more pre-defined clusters. For example there are clusters for smart metering applications, lighting (RGB, Hue, etc.), home automation and so on.

After using several different sensors and light bulbs which are all supporting ZigBee, i thought about making my own ZigBee end device. For that i have chosen the TI CC2530/31 chipset as SOC. This chip is relatively easy available, also on some chinese market portal 😉 and it come with a well documented firmware library called Z-Stack.

The Z-Stack makes it much more easy to implement your own custom device with ZigBee functionality. In the earlier days i always thought that i should and must code all these things by my self. This because i didn’t know about the existence of such stacks or just because i thought that i must understand the deepest inner processes of such a network. Today i can say that it was not that bad to get these experience. But these days, my projects are much more complex than before and i like to get jump started with the help of already working code bases.

Today i’m gonna show you how you can add your own, custom, attribute to a Z-Stack based application and how you can write to it from a zigbee2mqtt environment using mqtt and zigbee sheperd converters.

What you will need:

Open up an example Project from your Z-STACK folder. For example SampleLight.eww

Under App/zcl_samplelight.c you can find this line of code:

  // Register the application's attribute list
  zcl_registerAttrList( SAMPLELIGHT_ENDPOINT, zclSampleLight_NumAttributes, zclSampleLight_Attrs );

This is where we register all of our attributes to the underlaying operating system functions and methods.

So what happens, when our device receives a write request for a attribute? This event will call zclProcessInWriteCmd in the file zcl.c on line 4376

#ifdef ZCL_WRITE
/*********************************************************************
 * @fn      processInWriteCmd
 *
 * @brief   Process the "Profile" Write and Write No Response Commands
 *
 * @param   pInMsg - incoming message to process
 *
 * @return  TRUE if command processed. FALSE, otherwise.
 */
static uint8 zclProcessInWriteCmd( zclIncoming_t *pInMsg )
{
  zclWriteCmd_t *writeCmd;
  zclWriteRspCmd_t *writeRspCmd;
  uint8 sendRsp = FALSE;
  uint8 j = 0;
  uint8 i;

  writeCmd = (zclWriteCmd_t *)pInMsg->attrCmd;
  if ( pInMsg->hdr.commandID == ZCL_CMD_WRITE )
  {
    // We need to send a response back - allocate space for it
    writeRspCmd = (zclWriteRspCmd_t *)zcl_mem_alloc( sizeof( zclWriteRspCmd_t )
            + sizeof( zclWriteRspStatus_t ) * writeCmd->numAttr );
    if ( writeRspCmd == NULL )
    {
      return FALSE; // EMBEDDED RETURN
    }

    sendRsp = TRUE;
  }

  for ( i = 0; i < writeCmd->numAttr; i++ )
  {
    zclAttrRec_t attrRec;
    zclWriteRec_t *statusRec = &(writeCmd->attrList[i]);

    if ( zclFindAttrRec( pInMsg->msg->endPoint, pInMsg->msg->clusterId,
                         statusRec->attrID, &attrRec ) )
    {
      
      
      if ( statusRec->dataType == attrRec.attr.dataType )
      {
        uint8 status;

        // Write the new attribute value
        if ( attrRec.attr.dataPtr != NULL )

This function will search in our attributes list whether there is a attribute defined which match’s to the actually received write request. The actual request is stored inside of statusRec

But to be able to reach this function and therefore write into any attributes, you must enable ZCL_WRITE in your configuration. You can do this with preprocessor directives.

Since we know, how the system handles the attributes, we must add our own attribute to this list. The attributes are defined in the file zcl_samplelight_data.c – beginning on line 193

{
    ZCL_CLUSTER_ID_GEN_BASIC,             // Cluster IDs - defined in the foundation (ie. zcl.h)
    {  // Attribute record
      ATTRID_BASIC_HW_VERSION,            // Attribute ID - Found in Cluster Library header (ie. zcl_general.h)
      ZCL_DATATYPE_UINT8,                 // Data Type - found in zcl.h
      ACCESS_CONTROL_READ,                // Variable access control - found in zcl.h
      (void *)&zclSampleLight_HWRevision  // Pointer to attribute variable
    }
  },

The above code shows one sample attribute. Now we will define our own, and append it at the end of the list.

// My custom attribute
  {
    ZCL_CLUSTER_ID_GEN_BASIC,
    { // Attribute record
      0x4010, //Custom Attribute ID
      ZCL_DATATYPE_UINT16,
      (ACCESS_CONTROL_READ | ACCESS_REPORTABLE | ACCESS_CONTROL_WRITE),
      (void *)&REG_MY_CUSTOM_REGISTER
    }
  }

The above code shows our own attribute. I have used the GEN_BASIC Cluster and defined a UINT16 datatype. I saw some inline commentaries where some one wrote that there are problems with uint8 and boolean’s. If you ever face problems with them, try uint16 instead.

Now we must get informed about changes in our attribute. Unfortunately there doesn’t seem to be a out of the box solution for that. There is one for pre defined attribute id’s but we have defined our own. So i have found a workaround which seems to do the job quite good.

Whenever an event occurs, the system will jump into an event loop declared in the file zcl_samplelight.c on line 403. There are several cases, but write to an attribute does not fullfill one of them, so we will hook into the default case.

  default:
    //Check if a variable has been updated!
    if(REG_MY_CUSTOM_REGISTER_OLD != REG_MY_CUSTOM_REGISTER)
    {
      REG_MY_CUSTOM_REGISTER_OLD = REG_MY_CUSTOM_REGISTER;
    }        
    break;

Here i check if the register has changed. If so, i can execute my desired code section. This method of observation for changes has the benefit, that it doesn’t poll for changes. We will get informed about them. Ok not explicit about changes which concerns our attribute, but the overhead that it will produce if you have multiple attributes inside of the default case will still be marginal.

Adding converters to zigbee2mqtt

Now we would like to be able to set the attributes value from zigbee2mqtt. For this we first must define a new device. https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html

The above link will give you some basic informations about that process. If you have added your device, then you must provide a „ToZigbee“ converter.

In your devices.js you must provide something linke this:
toZigbee: [tz.FooBar],

The implementation of tz.FooBar in the file toZigbee.js will look like this:

FooBar: {
        key: ['MyAttributeKey'],
        convert: (key, value, message, type, postfix, options) => {
            if (type === 'set') {
                if (value === 'default') {
                    value = 1;
                }
				const lookup = {
                'Right': '0',
                'Left': '1',
                'IDLE': '2',
				};
				
				value = lookup[value];
				if( ((value >= 0) && value < 3) == false ) value = 2;
				
                return [{
                    cid: 'genBasic',
                    cmd: 'write',
                    cmdType: 'foundation',
                    zclData: [{
                        attrId: 0x4010,
                        dataType: 0x21,
                        attrData: value,
                    }],
                    cfg: cfg.default,
                }];
            }
        },
    },

Let me explain the above code. The first thing that we will see is the MyAttributeKey parameter. This will be the parameter under which you can set your attribute using mqtt. I will show you that later. On line 8 we have a lookup table for the conversion of string into numbers. This is optional but gives some advantages. On line 14 we will convert the input from a string into a number. On the next line we will check for a valid value. Line 17 is responsible for building our zigbee packet.

We must also define the cluster id (cid) identical to what we have defined in our firmware so far. Attribute ID, data type must also correspond accordingly. For the data type there is a list available on the internet which shows you the different types. http://www.zigbee.org/wp-content/uploads/2014/10/07-5123-06-zigbee-cluster-library-specification.pdf
Beginning on page 85. Be careful to check for the correct class!

If you have written your configuration file it’s time to restart zigbee2mqtt.

Test your work!
Now set a breakpoint here and start debugging.

  default:
    //Check if a variable has been updated!
    if(REG_MY_CUSTOM_REGISTER_OLD != REG_MY_CUSTOM_REGISTER)
    {
      REG_MY_CUSTOM_REGISTER_OLD = REG_MY_CUSTOM_REGISTER;
    }        
    break;

After the device shows up in zigbee2mqtt you should be able to send new attribute values to the device using mqtt.

Open up mqtt.fx, configure your server and connect. Now go to the publish tab and enter the following string:

zigbee2mqtt/YOUR UNIQUE IDENTIFIER/set/MyAttributeKey

Replace YOUR UNIQUE IDENTIFIER with your identifier which shows up in the console log of zigbee2mqtt.

Now you can send some values from mqtt.fx to your server and zigbee2mqtt should deliver these messages to your device. This will show something like this:

zigbee2mqtt:info XX/XX/XXXX, XX:XX:XX PM Zigbee publish to device '0x00124xxxxxxxxxx', genBasic - write - 
[{"attrId":16400,"dataType":33,"attrData":"1"}] - {"manufSpec":0,"disDefaultRsp":0} - 8

You should also hit your breakpoint now, and see the new value which was assigned to your attribute!

Well done! Hope this was helpful to someone.