Unable to write attribute to Zigbee device

I am new to homey app development, and I am basically trying to do one specific thing, which is to change an attribute in a Zigbee device that I have. The device is a Tuya valve controller, and it is supported in this app:

The problem is that the app does not support changing any attributes, and the valve controller does have an attribute/setting that controls the power on behaviour. By default it will close the valve when power is restored after a power loss, which I really don’t want. The device can be set to either open, close or last state, but it is tucked away in a custom cluster (if I am using the wrong terminology here, please excuse me).

I found some code for a different system which shows me the cluster and attribute ID. This thread has the info: [RELEASE] Tuya Zigbee Valve driver (w/ healthStatus) - Custom Drivers - Hubitat

The cluster ID is 57345, and the attribute in question is 53264. What I have done is to clone the app mentioned above, and I have tried to add that cluster as a custom cluster, and when I try to read that attribute, I get this response in the log:

2023-06-03T13:43:53.053Z zigbee-clusters:cluster ep: 1, cl: powerOnstate (57345) read attributes [ 53264 ]
2023-06-03T13:43:53.077Z zigbee-clusters:cluster ep: 1, cl: powerOnstate (57345) send frame ZCLStandardHeader {
  frameControl: [],
  data: powerOnstate.readAttributes { attributes: [ 53264 ] },
  cmdId: 0,
  trxSequenceNumber: 1
}
2023-06-03T13:43:53.272Z zigbee-clusters:cluster ep: 1, cl: powerOnstate (57345) received frame readAttributesStructured.response powerOnstate.readAttributesStructured.response {
  attributes: <Buffer 10 d0 00 30 00>
}
2023-06-03T13:43:53.290Z zigbee-clusters:cluster ep: 1, cl: powerOnstate (57345) read attributes result { attributes: <Buffer 10 d0 00 30 00> }

powerOnstate is the name I have given to this cluster. From what I can tell, I am actually getting something back from the device, but I am not sure why I am getting what looks to be three bytes (00 30 00), when the value apparently should be a byte (or an enum8).

The default value in the device is 0, so I am guessing that the 30 is actually ascii zero, so I probably want to change that to 32, but writing always fails in a way similar to this:

2023-06-03T13:43:53.303Z zigbee-clusters:cluster ep: 1, cl: powerOnstate (57345) send frame ZCLStandardHeader {
  frameControl: [],
  data: powerOnstate.writeAttributes { attributes: <Buffer 10 d0 29 32 00> },
  cmdId: 2,
  trxSequenceNumber: 2
}
2023-06-03T13:43:53.477Z zigbee-clusters:cluster ep: 1, cl: powerOnstate (57345) received frame writeAttributesAtomic.response powerOnstate.writeAttributesAtomic.response {
  attributes: [ AttributeResponse { status: 'INVALID_DATA_TYPE', id: 53264 } ]
}

I am not sure if the INVALID_DATA_TYPE is something returned from the device, but it does look like Homey actually tried to send something (at least it prints what looks like the buffer to send).

In this specific instance, I tried to write the attribute with this call:

await this.zclNode.endpoints[1].clusters.powerOnstate.writeAttributes({ powerOnstate: 0x000032 });

Not sure why the buffer that Homey tries to write contains the value 29, but I have been unable to find some proper docs to understand this.

I’ve tried changing the data type for this attribute to different types, but I can never seem to get Homey to write something that appears to generate a buffer that looks like the one read. This is my cluster spec file where the data type is defined:

'use strict';

const Cluster = require('../Cluster');
const { ZCLDataTypes } = require('../zclTypes');

const ATTRIBUTES = {
    powerOnstate: { id: 53264, type: ZCLDataTypes.int16 },
};

const COMMANDS = {};

class PoweronstateCluster extends Cluster {

  static get ID() {
    return 57345;
  }

  static get NAME() {
    return 'powerOnstate';
  }

  static get ATTRIBUTES() {
    return ATTRIBUTES;
  }

  static get COMMANDS() {
    return COMMANDS;
  }

}

Cluster.addCluster(PoweronstateCluster);

module.exports = PoweronstateCluster;

I have tried various data types, but I am really not understanding properly what I am doing. Not sure if it is possible to make sense of any of what I wrote, but if anyone can offer some pointers for me, I’d be grateful.

I don’t need to be able to change this from within the app, I just want to get it written to the device, and then I’ll just use the original app to control the valve. I am assuming that this setting is stored in nonvolatile memory in the device, so once changed, it will stay changed.

0x30 is the type identifier value for enum8.

The data shown when reading the attribute breaks down to:

10 d0 00 30 00
^^ ^^            0xd010 = Power On Behaviour attribute
      ^^         0x00   = Read Attributes Status (00 = Success, I think)
         ^^      0x30   = Type Identifier
            ^^   0x00   = Value (0x00 = Off, 0x01 = On, 0x02 = Last State)

(sources: ZCL specification, page 54 and this comment).

I’ve never worked on Zigbee apps on Homey so can’t provide a definitive answer, but I would expect that the following data needs to be sent (page 56 of the ZCL specification, “Write Attributes Command Frame Format”):

<Buffer 10 d0 30 02>

So try some of these:

.writeAttributes({ powerOnstate: 0x3002 })
.writeAttributes({ powerOnstate: Buffer.from('3002', 'hex') })
.writeAttributes({ powerOnstate: 0x10d03002 })
.writeAttributes({ powerOnstate: Buffer.from('10d03002', 'hex') })

Thanks!
I did try your suggestions, but somehow it seems impossible to get Homey to actually send the data that I want.

The ID is always added automatically (i.e 10 D0), but no matter which data type I specify for this attribute, Homey seems to add some extra byte(s), and the device (understandably) returns the INVALID_DATA_TYPE response. Even if I write the data as a buffer (as you suggested) some data is added, which I guess is what causes the problem.

The correct data type to use is probably enum8, but if I use that, the readAttributes call throws an error. The closest I got was when using uint16 as the data type for the attribute (in my initial post I had it as int16) and trying to write like this:

await this.zclNode.endpoints[1].clusters.powerOnstate.writeAttributes({ powerOnstate: 0x0230 });

What Homey then actually writes is this:

2023-06-03T18:40:33.562Z zigbee-clusters:cluster ep: 1, cl: powerOnstate (57345) write attributes { powerOnstate: 560 }
2023-06-03T18:40:33.564Z zigbee-clusters:cluster ep: 1, cl: powerOnstate (57345) send frame ZCLStandardHeader {
  frameControl: [],
  data: powerOnstate.writeAttributes { attributes: <Buffer 10 d0 21 30 02> },
  cmdId: 2,
  trxSequenceNumber: 2
}
[log] 2023-06-03 18:40:33 [ManagerDrivers] [Driver:valvecontroller] [Device:8181335a-8c48-4cef-b7c5-f03b1136e235] Power on status supported by device:  { powerOnstate: 0 }
2023-06-03T18:40:33.749Z zigbee-clusters:cluster ep: 1, cl: powerOnstate (57345) received frame writeAttributesAtomic.response powerOnstate.writeAttributesAtomic.response {
  attributes: [ AttributeResponse { status: 'INVALID_DATA_TYPE', id: 53264 } ]
}

That extra byte with value 21 should probably not be there, but I can’t seem to find a way to write the attribute to make the device happy. I’m sure it is possible, but my lack of knowledge and experience makes it very difficult.

It looks like there’s an additional (random) byte being added. I would assume because of this:

powerOnstate: { id: 53264, type: ZCLDataTypes.int16 }

That’s not the correct type.

I agree it is not the correct type, but I can’t seem to find a type that will work. I would have thought that enum8 would be the correct type, and that Homey then would know to add the 30 by itself, but that seems to not work either, as I said. Using enum8 as the type makes readAttributes throw an error.

I think these are all the data types to choose from:

const DataTypes = {
  noData      :               new DataType(0  , 'noData'       , 0   , (buf, v, i) => null   , (buf, i) => ({result:null, length: 0})),

  data8       :               new DataType(8  , 'data8'        , 1   , uintToBufBE           , uintFromBufBE               ),
  data16      :               new DataType(9  , 'data16'       , 2   , uintToBufBE           , uintFromBufBE               ),
  data24      :               new DataType(10 , 'data24'       , 3   , uintToBufBE           , uintFromBufBE               ),
  data32      :               new DataType(11 , 'data32'       , 4   , uintToBufBE           , uintFromBufBE               ),
  data40      :               new DataType(12 , 'data40'       , 5   , dataToBuf             , dataFromBuf                 ),
  data48      :               new DataType(13 , 'data48'       , 6   , dataToBuf             , dataFromBuf                 ),
  data56      :               new DataType(14 , 'data56'       , 7   , dataToBuf             , dataFromBuf                 ),
  data64      :               new DataType(15 , 'data64'       , 8   , dataToBuf             , dataFromBuf                 ),

  bool        :               new DataType(16 , 'bool'         , 1   , boolToBuf             , boolFromBuf                 ),

  map8        :  (...arg) =>  new DataType(24 , 'map8'         , 1   , bitmapToBuf           , bitmapFromBuf     , ...arg  ),
  map16       :  (...arg) =>  new DataType(25 , 'map16'        , 2   , bitmapToBuf           , bitmapFromBuf     , ...arg  ),
  map24       :  (...arg) =>  new DataType(26 , 'map24'        , 3   , bitmapToBuf           , bitmapFromBuf     , ...arg  ),
  map32       :  (...arg) =>  new DataType(27 , 'map32'        , 4   , bitmapToBuf           , bitmapFromBuf     , ...arg  ),
  map40       :  (...arg) =>  new DataType(28 , 'map40'        , 5   , bitmapToBuf           , bitmapFromBuf     , ...arg  ),
  map48       :  (...arg) =>  new DataType(29 , 'map48'        , 6   , bitmapToBuf           , bitmapFromBuf     , ...arg  ),
  map56       :  (...arg) =>  new DataType(30 , 'map56'        , 7   , bitmapToBuf           , bitmapFromBuf     , ...arg  ),
  map64       :  (...arg) =>  new DataType(31 , 'map64'        , 8   , bitmapToBuf           , bitmapFromBuf     , ...arg  ),

  uint8       :               new DataType(32 , 'uint8'        , 1   , uintToBuf             , uintFromBuf                 ),
  uint16      :               new DataType(33 , 'uint16'       , 2   , uintToBuf             , uintFromBuf                 ),
  uint24      :               new DataType(34 , 'uint24'       , 3   , uintToBuf             , uintFromBuf                 ),
  uint32      :               new DataType(35 , 'uint32'       , 4   , uintToBuf             , uintFromBuf                 ),
  uint40      :               new DataType(36 , 'uint40'       , 5   , uintToBuf             , uintFromBuf                 ),
  uint48      :               new DataType(37 , 'uint48'       , 6   , uintToBuf             , uintFromBuf                 ),

  //TODO:These exceed JS limits, turn to bigInts later
//uint56      :               new DataType(38 , 'uint56'       , 7   , uintToBuf             , uintFromBuf                 ),
//uint64      :               new DataType(39 , 'uint64'       , 8   , uintToBuf             , uintFromBuf                 ),

  int8        :               new DataType(40 , 'int8'         , 1   , intToBuf              , intFromBuf                  ),
  int16       :               new DataType(41 , 'int16'        , 2   , intToBuf              , intFromBuf                  ),
  int24       :               new DataType(42 , 'int24'        , 3   , intToBuf              , intFromBuf                  ),
  int32       :               new DataType(43 , 'int32'        , 4   , intToBuf              , intFromBuf                  ),
  int40       :               new DataType(44 , 'int40'        , 5   , intToBuf              , intFromBuf                  ),
  int48       :               new DataType(45 , 'int48'        , 6   , intToBuf              , intFromBuf                  ),

  //TODO:These exceed JS limits, turn to bigInts later
//int56       :               new DataType(46 , 'int56'        , 7   , intToBuf              , intFromBuf                  ),
//int64       :               new DataType(47 , 'int64'        , 8   , intToBuf              , intFromBuf                  ),

  enum8       :  (...arg) =>  new DataType(48 , 'enum8'        , 1   , enumToBuf             , enumFromBuf       , ...arg  ),
  enum16      :  (...arg) =>  new DataType(49 , 'enum16'       , 2   , enumToBuf             , enumFromBuf       , ...arg  ),
  enum32      :  (...arg) =>  new DataType(NaN, 'enum32'       , 4   , enumToBuf             , enumFromBuf       , ...arg  ),

  //TODO: javascript has no native semi precision floats
//semi        :               new DataType(56 , 'semi'         , 2   , semiToBuf             , semiFromBuf                 ),
  single      :               new DataType(57 , 'single'       , 4   , floatToBuf            , floatFromBuf                ),
  double      :               new DataType(58 , 'double'       , 8   , doubleToBuf           , doubleFromBuf               ),

  octstr      :               new DataType(65 , 'octstr'       , -1  , bufferToBuf           , bufferFromBuf               ),
  string      :               new DataType(66 , 'string'       , -1  , utf8StringToBuf       , utf8StringFromBuf           ),
// octstr16    :               new DataType(67 , 'octstr16'     , -2  , ),
// string16    :               new DataType(68 , 'string16'     , -2  , ),

// array       :               new DataType(72 , 'array'        , -1  , ),
// struct      :               new DataType(76 , 'struct'       , -1  , ),
// set         :               new DataType(80 , 'set'          , -1  , ),
// bag         :               new DataType(81 , 'bag'          , -1  , ),

// ToD         :               new DataType(224, 'ToD'          , 4   , ),
// date        :               new DataType(225, 'date'         , 4   , ),
// UTC         :               new DataType(226, 'UTC'          , 4   , ),

// clusterId   :               new DataType(232, 'clusterId'    , 2   , ),
// attribId    :               new DataType(233, 'attribId'     , 2   , ),

// bacOID      :               new DataType(234, 'bacOID'       , 4   , ),
  EUI64       :               new DataType(240, 'EUI64'        , 8   , EUI64ToBuf            , EUI64FromBuf                ),
  key128      :               new DataType(241, 'key128'       , 16  , key128ToBuf           , key128FromBuf               ),

// unk         :               new DataType(255, 'unk'          , 0   , ),

  ///INTERNAL TYPES
  uint4       :               new DataType(NaN, 'uint4'        , 0.5 , uint4ToBuf            , uint4FromBuf                ),
  enum4       :  (...arg) =>  new DataType(NaN, 'enum4'        , 0.5 , enum4ToBuf            , enum4FromBuf      , ...arg  ),
  map4        :  (...arg) =>  new DataType(NaN, 'map4'         , 0.5 , bitmap4ToBuf          , bitmap4FromBuf    , ...arg  ),

  buffer      :               new DataType(NaN, '_buffer'      , -0  , bufferToBuf           , bufferFromBuf               ),
  buffer8     :               new DataType(NaN, '_buffer8'     , -1  , bufferToBuf           , bufferFromBuf               ),
  buffer16    :               new DataType(NaN, '_buffer16'    , -2  , bufferToBuf           , bufferFromBuf               ),
  Array0      :  (...arg) =>  new DataType(NaN, '_Array0'      , -0  , arrayToBuf            , arrayFromBuf      , ...arg  ),
  Array8      :  (...arg) =>  new DataType(NaN, '_Array8'      , -1  , arrayToBuf            , arrayFromBuf      , ...arg  ),
  FixedString :     (len) =>  new DataType(NaN, '_FixedString' , len , utf8FixedStringToBuf  , utf8FixedStringFromBuf      ),
};

Success!

It seems that to use the enum8 type, it is necessary to define the values it can take, so I did this in my cluster definition file (or whatever it should be called):

const ATTRIBUTES = {
    powerOnstate: { id: 53264, type: ZCLDataTypes.enum8 ({
        Off: 0,
        On: 1,
        Remember: 2,
    })
 },
};

After that, I could read the attribute, and I could also write it like so:

await this.zclNode.endpoints[1].clusters.powerOnstate.writeAttributes({ powerOnstate: 2 });

The log then shows a sweet success:

2023-06-03T22:09:45.242Z zigbee-clusters:cluster ep: 1, cl: powerOnstate (57345) write attributes { powerOnstate: 2 }
2023-06-03T22:09:45.249Z zigbee-clusters:cluster ep: 1, cl: powerOnstate (57345) send frame ZCLStandardHeader {
  frameControl: [],
  data: powerOnstate.writeAttributes { attributes: <Buffer 10 d0 30 02> },
  cmdId: 2,
  trxSequenceNumber: 2
}
2023-06-03T22:09:45.350Z zigbee-clusters:cluster ep: 1, cl: powerOnstate (57345) received frame writeAttributesAtomic.response powerOnstate.writeAttributesAtomic.response {
  attributes: [ AttributeResponse { status: 'SUCCESS', id: 0 } ]
}

The data bytes written is now correct (and it is exactly what @robertklep suggested), and the setting is stored in the device, and it works as it should!

Again, thanks for your help, Robert!

It would have been better if this had been added to the app itself, but what I have done is just a hack to get the setting stored in the device, so I hope that @johan_bendz can add it properly to the app with a user-adjustable setting, as it really should be there. Having your water closed every time power has blipped is not a good thing.

1 Like

Nothing personal, but I just wonder a bit why some members mark their own post as solution to their own question (it happens every now and then).
Besides it looks a bit like showing off:
in this case, I really got the impression Robert @robertklep got you in the right direction to solve your issues, and he should get the credits imho.
This is also in general for members giving other members a hand, not about Robert in particular :upside_down_face:

But maybe I just see things wrong, in that case it is best to just ignore this message :hugs:

The post @mroek marked as solution is the one that actually contains the solution, so I don’t see the problem :smiley:

2 Likes

I have unmarked it now, it was never meant as showing off. I’d have thought that my posts really shows that I am inexperienced in Homey app programming, and that showing off was clearly not my intention.

And yes, as I wrote, Robert really helped pushing me in the correct direction, which I am very thankful for. I can’t mark any of his posts as the actual solution, so in that case I’d better leave no posts as the solution, so that people reading the thread can see that I received valuable help in getting to a solution, even if I did discover the last little piece of the puzzle myself (which was adding the allowed values to the enum8).

1 Like

My apologies, mroek.
Again, it was and is not personal, but I probably should’ve created a new topic for my thoughts.

I think I just did not get the ‘solution system’ 100% right :blush::face_with_hand_over_mouth::hugs:

It’s probably not about who pointed the topic starter to the final solution (to receive the ‘credits’ so to speak);
it’s probably about how users with a similar question can find a solution right away, when a topic is marked as ‘Solved’.

Please feel free to mark your solution to your question as such.
Thanks. Also thanks to @robertklep

Hi @mroek, well done with the coding :slight_smile: (and @robertklep too :slight_smile: )

I started coding on a TuyaPowerOnStateCluster some time ago but have had no time to finish it, I’ll put some effort into it. I’ll try to have an update of the test code this week.

2 Likes

Sounds great! I am sorry that my fix was just a temporary hack to get the value written to the device, and as such what I did is of no help to you.

Having it added as a user-adjustable property is obviously the best way, and I think there are quite a few other Tuya devices that uses the exact same cluster and values for this purpose, so it will most likely benefit many devices if you can add it the proper way.