Merge pull request #10074 from s-hadinger/zigbee_tuya_write

Zigbee better support for Tuya Protocol
This commit is contained in:
s-hadinger 2020-12-06 19:44:15 +01:00 committed by GitHub
commit 9051240121
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 248 additions and 118 deletions

View File

@ -177,6 +177,13 @@ public:
}
return 0;
}
uint32_t get32BigEndian(const size_t offset) const {
if (offset < len() - 3) {
return _buf->buf[offset+3] | (_buf->buf[offset+2] << 8) |
(_buf->buf[offset+1] << 16) | (_buf->buf[offset] << 24);
}
return 0;
}
int32_t get32IBigEndian(const size_t offset) const {
if (offset < len() - 3) {
return _buf->buf[offset+3] | (_buf->buf[offset+2] << 8) |

View File

@ -32,6 +32,7 @@ enum class Z_Data_Type : uint8_t {
Z_Alarm = 4,
Z_Thermo = 5, // Thermostat and sensor for home environment (temp, himudity, pressure)
Z_OnOff = 6, // OnOff, Buttons and Relays (always complements Lights and Plugs)
Z_Mode = 0xE, // Encode special modes for communication, like Tuya Zigbee protocol
Z_Ext = 0xF, // extended for other values
Z_Device = 0xFF // special value when parsing Device level attributes
};
@ -43,8 +44,7 @@ const uint8_t Z_Data_Type_char[] PROGMEM = {
'I', // 0x03 Z_Data_Type::Z_PIR
'A', // 0x04 Z_Data_Type::Z_Alarm
'T', // 0x05 Z_Data_Type::Z_Thermo
'O', // 0x05 Z_Data_Type::Z_OnOff
'\0', // 0x06
'O', // 0x06 Z_Data_Type::Z_OnOff
'\0', // 0x07
'\0', // 0x08
'\0', // 0x09
@ -52,7 +52,7 @@ const uint8_t Z_Data_Type_char[] PROGMEM = {
'\0', // 0x0B
'\0', // 0x0C
'\0', // 0x0D
'\0', // 0x0E
'~', // 0x0E
'E', // 0x0F Z_Data_Type::Z_Ext
// '_' maps to 0xFF Z_Data_Type::Z_Device
};
@ -500,6 +500,32 @@ void Z_Data_Alarm::convertZoneStatus(Z_attribute_list & attr_list, uint16_t val)
}
}
/*********************************************************************************************\
* Mode
*
// List of modes
// 0x1 = Tuya Zigbee mode
// 0xF (default) = ZCL standard mode
\*********************************************************************************************/
enum Z_Mode_Type {
ZM_Tuya = 0x1,
};
class Z_Data_Mode : public Z_Data {
public:
Z_Data_Mode(uint8_t endpoint = 0) :
Z_Data(Z_Data_Type::Z_Mode, endpoint)
{}
inline bool isTuyaProtocol(void) const { return _config == 1; }
static const Z_Data_Type type = Z_Data_Type::Z_Mode;
};
/*********************************************************************************************\
*
\*********************************************************************************************/
const uint8_t Z_Data_Type_len[] PROGMEM = {
0, // 0x00 Z_Data_Type::Z_Unknown
sizeof(Z_Data_Light), // 0x01 Z_Data_Type::Z_Light
@ -507,8 +533,7 @@ const uint8_t Z_Data_Type_len[] PROGMEM = {
sizeof(Z_Data_PIR), // 0x03 Z_Data_Type::Z_PIR
sizeof(Z_Data_Alarm), // 0x04 Z_Data_Type::Z_Alarm
sizeof(Z_Data_Thermo), // 0x05 Z_Data_Type::Z_Thermo
sizeof(Z_Data_OnOff), // 0x05 Z_Data_Type::Z_OnOff
0, // 0x06
sizeof(Z_Data_OnOff), // 0x06 Z_Data_Type::Z_OnOff
0, // 0x07
0, // 0x08
0, // 0x09
@ -516,7 +541,7 @@ const uint8_t Z_Data_Type_len[] PROGMEM = {
0, // 0x0B
0, // 0x0C
0, // 0x0D
0, // 0x0E
sizeof(Z_Data_Mode), // 0x0E
0, // 0x0F Z_Data_Type::Z_Ext
// '_' maps to 0xFF Z_Data_Type::Z_Device
};
@ -549,6 +574,8 @@ public:
template <class M>
M & get(uint8_t ep = 0);
template <class M>
const M & getConst(uint8_t ep = 0) const;
template <class M>
const M & find(uint8_t ep = 0) const;
@ -569,6 +596,7 @@ bool Z_Data_Set::updateData(Z_Data & elt) {
case Z_Data_Type::Z_Thermo: return ((Z_Data_Thermo&) elt).update(); break;
case Z_Data_Type::Z_OnOff: return ((Z_Data_OnOff&) elt).update(); break;
case Z_Data_Type::Z_PIR: return ((Z_Data_PIR&) elt).update(); break;
case Z_Data_Type::Z_Mode: return ((Z_Data_Mode&) elt).update(); break;
default: return false;
}
}
@ -587,6 +615,8 @@ Z_Data & Z_Data_Set::getByType(Z_Data_Type type, uint8_t ep) {
return get<Z_Data_OnOff>(ep);
case Z_Data_Type::Z_PIR:
return get<Z_Data_PIR>(ep);
case Z_Data_Type::Z_Mode:
return get<Z_Data_Mode>(ep);
default:
return *(Z_Data*)nullptr;
}
@ -626,6 +656,11 @@ M & Z_Data_Set::get(uint8_t ep) {
M & m = (M&) find(M::type, ep);
return addIfNull<M>(m, ep);
}
template <class M>
const M & Z_Data_Set::getConst(uint8_t ep) const {
const M & m = (M&) find(M::type, ep);
return m;
}
template <class M>
const M & Z_Data_Set::find(uint8_t ep) const {
@ -917,6 +952,7 @@ public:
// Find device by name, can be short_addr, long_addr, number_in_array or name
Z_Device & parseDeviceFromName(const char * param, bool short_must_be_known = false);
bool isTuyaProtocol(uint16_t shortaddr, uint8_t ep = 0) const;
private:
LList<Z_Device> _devices; // list of devices

View File

@ -830,13 +830,16 @@ int32_t Z_Devices::deviceRestore(JsonParserObject json) {
for (auto config_elt : arr_config) {
const char * conf_str = config_elt.getStr();
Z_Data_Type data_type;
uint8_t ep, config;
uint8_t ep = 0;
uint8_t config = 0xF; // default = no config
if (Z_Data::ConfigToZData(conf_str, &data_type, &ep, &config)) {
Z_Data & data = device.data.getByType(data_type, ep);
if (&data != nullptr) {
data.setConfig(config);
}
} else {
AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Ignoring config '%s'"), conf_str);
}
}
}
@ -848,6 +851,17 @@ Z_Data_Light & Z_Devices::getLight(uint16_t shortaddr) {
return getShortAddr(shortaddr).data.get<Z_Data_Light>();
}
bool Z_Devices::isTuyaProtocol(uint16_t shortaddr, uint8_t ep) const {
const Z_Device & device = findShortAddr(shortaddr);
if (device.valid()) {
const Z_Data_Mode & mode = device.data.getConst<Z_Data_Mode>(ep);
if (&mode != nullptr) {
return mode.isTuyaProtocol();
}
}
return false;
}
/*********************************************************************************************\
* Device specific data handlers
\*********************************************************************************************/

View File

@ -42,9 +42,12 @@ enum Z_DataTypes {
ZEUI64 = 0xF0, Zkey128 = 0xF1,
Zunk = 0xFF,
// adding fake type for Tuya specific encodings
Ztuya1 = 0x80, // 1 byte unsigned int, Big Endian when output (input is taken care of)
Ztuya2 = 0x81, // 2 bytes unsigned, Big Endian when output (input is taken care of)
Ztuya4 = 0x82, // 4 bytes signed, Big Endian when output (input is taken care of)
Ztuya0 = Zoctstr,
Ztuya1 = Zbool,
Ztuya2 = Zint32,
Ztuya3 = Zstring,
Ztuya4 = Zuint8,
Ztuya5 = Zuint32
};
//
@ -65,18 +68,13 @@ uint8_t Z_getDatatypeLen(uint8_t t) {
case Zsemi:
case ZclusterId:
case ZattribId:
case Ztuya1:
return 2;
case Ztuya2:
return 3;
case Zsingle:
case ZToD:
case Zdate:
case ZUTC:
case ZbacOID:
return 4;
case Ztuya4:
return 5;
case Zdouble:
case ZEUI64:
return 8;
@ -603,8 +601,15 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = {
{ Zuint8, Cx0B05, 0x011D, Z_(LastMessageRSSI), Cm1, 0 },
// Tuya Moes specific - 0xEF00
{ Zoctstr, CxEF00, 0x0070, Z_(TuyaScheduleWorkdays), Cm1, 0 },
{ Zoctstr, CxEF00, 0x0071, Z_(TuyaScheduleHolidays), Cm1, 0 },
// Mapping of Tuya type with internal mapping
// 0x00 - Zoctstr (len N)
// 0x01 - Ztuya1 (len 1) - equivalent to Zuint8 without invalid value handling
// 0x02 - Ztuya4 (len 4) - equivalent to Zint32 in big endian and without invalid value handling
// 0x03 - Zstr (len N)
// 0x04 - Ztuya1 (len 1)
// 0x05 - Ztuya4u (len 1/2/4) - equivalent to Zuint32
{ Ztuya0, CxEF00, 0x0070, Z_(TuyaScheduleWorkdays), Cm1, 0 },
{ Ztuya0, CxEF00, 0x0071, Z_(TuyaScheduleHolidays), Cm1, 0 },
{ Ztuya1, CxEF00, 0x0101, Z_(Power), Cm1, 0 },
{ Ztuya1, CxEF00, 0x0102, Z_(Power2), Cm1, 0 },
{ Ztuya1, CxEF00, 0x0103, Z_(Power3), Cm1, 0 },
@ -613,21 +618,21 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = {
{ Ztuya1, CxEF00, 0x0112, Z_(TuyaWindowDetection), Cm1, 0 },
{ Ztuya1, CxEF00, 0x0114, Z_(TuyaValveDetection), Cm1, 0 },
{ Ztuya1, CxEF00, 0x0174, Z_(TuyaAutoLock), Cm1, 0 },
{ Ztuya4, CxEF00, 0x0202, Z_(TuyaTempTarget), Cm_10, 0 },
{ Ztuya4, CxEF00, 0x0203, Z_(LocalTemperature), Cm_10, 0 }, // will be overwritten by actual LocalTemperature
{ Ztuya1, CxEF00, 0x0215, Z_(TuyaBattery), Cm1, 0 }, // TODO check equivalent?
{ Ztuya4, CxEF00, 0x0266, Z_(TuyaMinTemp), Cm1, 0 },
{ Ztuya4, CxEF00, 0x0267, Z_(TuyaMaxTemp), Cm1, 0 },
{ Ztuya4, CxEF00, 0x0269, Z_(TuyaBoostTime), Cm1, 0 },
{ Ztuya4, CxEF00, 0x026B, Z_(TuyaComfortTemp), Cm1, 0 },
{ Ztuya4, CxEF00, 0x026C, Z_(TuyaEcoTemp), Cm1, 0 },
{ Ztuya1, CxEF00, 0x026D, Z_(TuyaValvePosition), Cm1, 0 },
{ Ztuya4, CxEF00, 0x0272, Z_(TuyaAwayTemp), Cm1, 0 },
{ Ztuya4, CxEF00, 0x0275, Z_(TuyaAwayDays), Cm1, 0 },
{ Ztuya1, CxEF00, 0x0404, Z_(TuyaPreset), Cm1, 0 },
{ Ztuya1, CxEF00, 0x0405, Z_(TuyaFanMode), Cm1, 0 },
{ Ztuya1, CxEF00, 0x046A, Z_(TuyaForceMode), Cm1, 0 },
{ Ztuya1, CxEF00, 0x046F, Z_(TuyaWeekSelect), Cm1, 0 },
{ Ztuya2, CxEF00, 0x0202, Z_(TuyaTempTarget), Cm_10, 0 },
{ Ztuya2, CxEF00, 0x0203, Z_(LocalTemperature), Cm_10, 0 }, // will be overwritten by actual LocalTemperature
{ Ztuya2, CxEF00, 0x0215, Z_(TuyaBattery), Cm1, 0 }, // TODO check equivalent?
{ Ztuya2, CxEF00, 0x0266, Z_(TuyaMinTemp), Cm1, 0 },
{ Ztuya2, CxEF00, 0x0267, Z_(TuyaMaxTemp), Cm1, 0 },
{ Ztuya2, CxEF00, 0x0269, Z_(TuyaBoostTime), Cm1, 0 },
{ Ztuya2, CxEF00, 0x026B, Z_(TuyaComfortTemp), Cm1, 0 },
{ Ztuya2, CxEF00, 0x026C, Z_(TuyaEcoTemp), Cm1, 0 },
{ Ztuya2, CxEF00, 0x026D, Z_(TuyaValvePosition), Cm1, 0 },
{ Ztuya2, CxEF00, 0x0272, Z_(TuyaAwayTemp), Cm1, 0 },
{ Ztuya2, CxEF00, 0x0275, Z_(TuyaAwayDays), Cm1, 0 },
{ Ztuya4, CxEF00, 0x0404, Z_(TuyaPreset), Cm1, 0 },
{ Ztuya4, CxEF00, 0x0405, Z_(TuyaFanMode), Cm1, 0 },
{ Ztuya4, CxEF00, 0x046A, Z_(TuyaForceMode), Cm1, 0 },
{ Ztuya4, CxEF00, 0x046F, Z_(TuyaWeekSelect), Cm1, 0 },
// Terncy specific - 0xFCCC
{ Zuint16, CxFCCC, 0x001A, Z_(TerncyDuration), Cm1, 0 },
@ -863,10 +868,6 @@ int32_t encodeSingleAttribute(class SBuffer &buf, double val_d, const char *val_
case Zmap8: // map8
buf.add8(u32);
break;
case Ztuya1: // tuya specific 1 byte
buf.add8(1); // len
buf.add8(u32);
break;
// unsigned 16
case Zuint16: // uint16
case Zenum16: // enum16
@ -874,9 +875,6 @@ int32_t encodeSingleAttribute(class SBuffer &buf, double val_d, const char *val_
case Zmap16: // map16
buf.add16(u32);
break;
case Ztuya2:
buf.add8(2); // len
buf.add16BigEndian(u32);
// unisgned 32
case Zuint32: // uint32
case Zdata32: // data32
@ -895,10 +893,6 @@ int32_t encodeSingleAttribute(class SBuffer &buf, double val_d, const char *val_
case Zint32: // int32
buf.add32(i32);
break;
case Ztuya4:
buf.add8(4); // len
buf.add32BigEndian(i32);
break;
case Zsingle: // float
buf.add32( *((uint32_t*)&f32) ); // cast float as uint32_t
@ -987,15 +981,6 @@ uint32_t parseSingleAttribute(Z_attribute & attr, const SBuffer &buf,
}
}
break;
case Ztuya1: // uint8 Big Endian
attr.setUInt(buf.get8(i+1));
break;
case Ztuya2: // uint16 Big Endian
attr.setUInt(buf.get16BigEndian(i+1));
break;
case Ztuya4:
attr.setInt(buf.get32IBigEndian(i+1));
break;
// Note: uint40, uint48, uint56, uint64 are displayed as Hex
// Note: int40, int48, int56, int64 are displayed as Hex
case Zuint40: // uint40
@ -1960,6 +1945,39 @@ void Z_postProcessAttributes(uint16_t shortaddr, uint16_t src_ep, class Z_attrib
}
}
// Internal search function
void Z_parseAttributeKey_inner(class Z_attribute & attr, uint16_t preferred_cluster) {
// scan attributes to find by name, and retrieve type
for (uint32_t i = 0; i < ARRAY_SIZE(Z_PostProcess); i++) {
const Z_AttributeConverter *converter = &Z_PostProcess[i];
uint16_t local_attr_id = pgm_read_word(&converter->attribute);
uint16_t local_cluster_id = CxToCluster(pgm_read_byte(&converter->cluster_short));
uint8_t local_type_id = pgm_read_byte(&converter->type);
int8_t local_multiplier = CmToMultiplier(pgm_read_byte(&converter->multiplier_idx));
// AddLog_P(LOG_LEVEL_DEBUG, PSTR("Try cluster = 0x%04X, attr = 0x%04X, type_id = 0x%02X"), local_cluster_id, local_attr_id, local_type_id);
if (!attr.key_is_str) {
if ((attr.key.id.cluster == local_cluster_id) && (attr.key.id.attr_id == local_attr_id)) {
attr.attr_type = local_type_id;
break;
}
} else if (pgm_read_word(&converter->name_offset)) {
const char * key = attr.key.key;
// AddLog_P(LOG_LEVEL_DEBUG, PSTR("Comparing '%s' with '%s'"), attr_name, converter->name);
if (0 == strcasecmp_P(key, Z_strings + pgm_read_word(&converter->name_offset))) {
if ((preferred_cluster == 0xFFFF) || // any cluster
(local_cluster_id == preferred_cluster)) {
// match
attr.setKeyId(local_cluster_id, local_attr_id);
attr.attr_type = local_type_id;
attr.attr_multiplier = local_multiplier;
break;
}
}
}
}
}
//
// Given an attribute string, retrieve all attribute details.
// Input: the attribute has a key name, either: <cluster>/<attr> or <cluster>/<attr>%<type> or "<attribute_name>"
@ -1976,7 +1994,7 @@ void Z_postProcessAttributes(uint16_t shortaddr, uint16_t src_ep, class Z_attrib
// Note: the attribute value is unchanged and unparsed
//
// Note: if the type is specified in the key, the multiplier is not applied, no conversion happens
bool Z_parseAttributeKey(class Z_attribute & attr) {
bool Z_parseAttributeKey(class Z_attribute & attr, uint16_t preferred_cluster) {
// check if the name has the format "XXXX/YYYY" where XXXX is the cluster, YYYY the attribute id
// alternative "XXXX/YYYY%ZZ" where ZZ is the type (for unregistered attributes)
if (attr.key_is_str) {
@ -2002,33 +2020,11 @@ bool Z_parseAttributeKey(class Z_attribute & attr) {
// AddLog_P(LOG_LEVEL_DEBUG, PSTR("cluster_id = 0x%04X, attr_id = 0x%04X"), cluster_id, attr_id);
// do we already know the type, i.e. attribute and cluster are also known
if ((Zunk == attr.attr_type) && (preferred_cluster != 0xFFFF)) {
Z_parseAttributeKey_inner(attr, preferred_cluster); // try to find with the selected cluster
}
if (Zunk == attr.attr_type) {
// scan attributes to find by name, and retrieve type
for (uint32_t i = 0; i < ARRAY_SIZE(Z_PostProcess); i++) {
const Z_AttributeConverter *converter = &Z_PostProcess[i];
uint16_t local_attr_id = pgm_read_word(&converter->attribute);
uint16_t local_cluster_id = CxToCluster(pgm_read_byte(&converter->cluster_short));
uint8_t local_type_id = pgm_read_byte(&converter->type);
int8_t local_multiplier = CmToMultiplier(pgm_read_byte(&converter->multiplier_idx));
// AddLog_P(LOG_LEVEL_DEBUG, PSTR("Try cluster = 0x%04X, attr = 0x%04X, type_id = 0x%02X"), local_cluster_id, local_attr_id, local_type_id);
if (!attr.key_is_str) {
if ((attr.key.id.cluster == local_cluster_id) && (attr.key.id.attr_id == local_attr_id)) {
attr.attr_type = local_type_id;
break;
}
} else if (pgm_read_word(&converter->name_offset)) {
const char * key = attr.key.key;
// AddLog_P(LOG_LEVEL_DEBUG, PSTR("Comparing '%s' with '%s'"), attr_name, converter->name);
if (0 == strcasecmp_P(key, Z_strings + pgm_read_word(&converter->name_offset))) {
// match
attr.setKeyId(local_cluster_id, local_attr_id);
attr.attr_type = local_type_id;
attr.attr_multiplier = local_multiplier;
break;
}
}
}
Z_parseAttributeKey_inner(attr, 0xFFFF); // try again with any cluster
}
return (Zunk != attr.attr_type) ? true : false;
}

View File

@ -460,6 +460,41 @@ void convertClusterSpecific(class Z_attribute_list &attr_list, uint16_t cluster,
}
}
void parseSingleTuyaAttribute(Z_attribute & attr, const SBuffer &buf,
uint32_t i, uint32_t len, int32_t attrtype) {
// fallback - enter a null value
attr.setNone(); // set to null by default
// now parse accordingly to attr type
switch (attrtype) {
case 0x00: // RAW octstr
attr.setBuf(buf, i, len);
break;
case 0x01: // Bool, values 0/1, len = 1
case 0x04: // enum 8 bits
attr.setUInt(buf.get8(i));
break;
case 0x02: // 4 bytes value (signed?)
attr.setUInt(buf.get32BigEndian(i));
break;
case 0x03: // String (we expect it is not ended with \00)
char str[len+1];
strncpy(str, buf.charptr(i), len);
str[len] = 0x00;
attr.setStr(str);
break;
case 0x05: // enum in 1/2/4 bytes, Big Endian
if (1 == len) {
attr.setUInt(buf.get8(i));
} else if (2 == len) {
attr.setUInt(buf.get16BigEndian(i));
} else if (4 == len) {
attr.setUInt(buf.get32BigEndian(i));
}
}
}
//
// Tuya - MOES specifc cluster 0xEF00
// See https://medium.com/@dzegarra/zigbee2mqtt-how-to-add-support-for-a-new-tuya-based-device-part-2-5492707e882d
@ -468,21 +503,14 @@ void convertClusterSpecific(class Z_attribute_list &attr_list, uint16_t cluster,
bool convertTuyaSpecificCluster(class Z_attribute_list &attr_list, uint16_t cluster, uint8_t cmd, bool direction, uint16_t shortaddr, uint8_t srcendpoint, const SBuffer &buf) {
// uint8_t status = buf.get8(0);
// uint8_t transid = buf.get8(1);
uint16_t dp = buf.get16(2); // decode dp as 16 bits little endian - which is not big endian as mentioned in documentation
// uint8_t fn = buf.get8(4);
uint8_t len = buf.get8(5); // len of payload
uint8_t dp = buf.get8(2); // dpid from Tuya documentation
uint8_t attr_type = buf.get8(3); // data type from Tuya documentation
uint16_t len = buf.get16BigEndian(4);
if ((1 == cmd) || (2 == cmd)) { // attribute report or attribute response
// create a synthetic attribute with id 'dp'
Z_attribute & attr = attr_list.addAttribute(cluster, dp);
uint8_t attr_type;
switch (len) {
case 1: attr_type = Ztuya1; break;
case 2: attr_type = Ztuya2; break;
case 4: attr_type = Ztuya4; break;
default: attr_type = Zoctstr; break;
}
parseSingleAttribute(attr, buf, 5, attr_type);
Z_attribute & attr = attr_list.addAttribute(cluster, (attr_type << 8) | dp);
parseSingleTuyaAttribute(attr, buf, 6, len, attr_type);
return true; // true = remove the original Tuya attribute
}
return false;

View File

@ -700,6 +700,7 @@ int32_t Z_ReceiveSimpleDesc(int32_t res, const class SBuffer &buf) {
const size_t numInIndex = 11;
const size_t numOutIndex = 12 + numInCluster*2;
#endif
bool is_tuya_protocol = false;
if (0 == status) {
// device is reachable
@ -710,14 +711,15 @@ int32_t Z_ReceiveSimpleDesc(int32_t res, const class SBuffer &buf) {
}
Response_P(PSTR("{\"" D_JSON_ZIGBEE_STATE "\":{"
"\"Status\":%d,\"Endpoint\":\"0x%02X\""
"\"Status\":%d,\"Device\":\"0x%04X\",\"Endpoint\":\"0x%02X\""
",\"ProfileId\":\"0x%04X\",\"DeviceId\":\"0x%04X\",\"DeviceVersion\":%d"
",\"InClusters\":["),
ZIGBEE_STATUS_SIMPLE_DESC, endpoint,
ZIGBEE_STATUS_SIMPLE_DESC, nwkAddr, endpoint,
profileId, deviceId, deviceVersion);
for (uint32_t i = 0; i < numInCluster; i++) {
if (i > 0) { ResponseAppend_P(PSTR(",")); }
ResponseAppend_P(PSTR("\"0x%04X\""), buf.get16(numInIndex + i*2));
if (buf.get16(numInIndex + i*2) == 0xEF00) { is_tuya_protocol = true; } // tuya protocol
}
ResponseAppend_P(PSTR("],\"OutClusters\":["));
for (uint32_t i = 0; i < numOutCluster; i++) {
@ -729,6 +731,14 @@ int32_t Z_ReceiveSimpleDesc(int32_t res, const class SBuffer &buf) {
XdrvRulesProcess();
}
// If tuya protocol, change the model information
if (is_tuya_protocol) {
Z_Device & device = zigbee_devices.getShortAddr(nwkAddr);
device.addEndpoint(endpoint);
device.data.get<Z_Data_Mode>(endpoint).setConfig(ZM_Tuya);
zigbee_devices.dirty();
}
return -1;
}
@ -2036,7 +2046,7 @@ void ZCLFrame::autoResponder(const uint16_t *attr_list_ids, size_t attr_len) {
break;
}
if (!attr.isNone()) {
Z_parseAttributeKey(attr);
Z_parseAttributeKey(attr, _cluster_id);
attr_list.addAttribute(_cluster_id, attr_id) = attr;
}
}

View File

@ -237,7 +237,7 @@ void ZbApplyMultiplier(double &val_d, int8_t multiplier) {
//
// Write Tuya-Moes attribute
//
bool ZbTuyaWrite(SBuffer & buf, const Z_attribute & attr, uint8_t transid) {
bool ZbTuyaWrite(SBuffer & buf, const Z_attribute & attr) {
double val_d = attr.getFloat();
const char * val_str = attr.getStr();
@ -245,13 +245,49 @@ bool ZbTuyaWrite(SBuffer & buf, const Z_attribute & attr, uint8_t transid) {
if (attr.isNum() && (1 != attr.attr_multiplier)) {
ZbApplyMultiplier(val_d, attr.attr_multiplier);
}
buf.add8(0); // status
buf.add8(transid); // transid
buf.add16(attr.key.id.attr_id); // dp
buf.add8(0); // fn
int32_t res = encodeSingleAttribute(buf, val_d, val_str, attr.attr_type);
if (res < 0) {
AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Error for Tuya attribute type %04X/%04X '0x%02X'"), attr.key.id.cluster, attr.key.id.attr_id, attr.attr_type);
uint32_t u32 = val_d;
int32_t i32 = val_d;
uint8_t tuyatype = (attr.key.id.attr_id >> 8);
uint8_t dpid = (attr.key.id.attr_id & 0xFF);
buf.add8(tuyatype);
buf.add8(dpid);
// the next attribute is length 16 bits in big endian
// high byte is always 0x00
buf.add8(0);
switch (tuyatype) {
case 0x00: // raw
{
SBuffer buf_raw = SBuffer::SBufferFromHex(val_str, strlen(val_str));
if (buf_raw.len() > 255) { return false; }
buf.add8(buf_raw.len());
buf.addBuffer(buf_raw);
}
break;
case 0x01: // Boolean = uint8
case 0x04: // enum uint8
buf.add8(1);
buf.add8(u32);
break;
case 0x02: // int32
buf.add8(4);
buf.add32BigEndian(i32);
break;
case 0x03: // String
{
uint32_t s_len = strlen(val_str);
if (s_len > 255) { return false; }
buf.add8(s_len);
buf.addBuffer(val_str, s_len);
}
break;
case 0x05: // bitmap 1/2/4 so we use 4 bytes
buf.add8(4);
buf.add32BigEndian(u32);
break;
default:
return false;
}
return true;
@ -296,13 +332,15 @@ void ZbSendReportWrite(class JsonParserToken val_pubwrite, class ZigbeeZCLSendMe
XdrvMailbox.command = (char*) ""; // prevent a crash when calling ReponseCmndChar and there was no previous command
}
bool tuya_protocol = zigbee_devices.isTuyaProtocol(packet.shortaddr, packet.endpoint);
// iterate on keys
for (auto key : val_pubwrite.getObject()) {
JsonParserToken value = key.getValue();
Z_attribute attr;
attr.setKeyName(key.getStr());
if (Z_parseAttributeKey(attr)) {
if (Z_parseAttributeKey(attr, tuya_protocol ? 0xEF00 : 0xFFFF)) { // favor tuya protocol if needed
// Buffer ready, do some sanity checks
// all attributes must use the same cluster
@ -337,14 +375,15 @@ void ZbSendReportWrite(class JsonParserToken val_pubwrite, class ZigbeeZCLSendMe
// Split encoding depending on message
if (packet.cmd != ZCL_CONFIGURE_REPORTING) {
if ((packet.cluster == 0XEF00) && (packet.cmd == ZCL_WRITE_ATTRIBUTES)) {
// special case of Tuya / Moes attributes
if (buf.len() > 0) {
AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Only 1 attribute allowed for Tuya"));
return;
// special case of Tuya / Moes / Lidl attributes
if (buf.len() == 0) {
// add the prefix to data
buf.add8(0); // status
buf.add8(zigbee_devices.getNextSeqNumber(packet.shortaddr));
}
packet.clusterSpecific = true;
packet.cmd = 0x00;
if (!ZbTuyaWrite(buf, attr, zigbee_devices.getNextSeqNumber(packet.shortaddr))) {
if (!ZbTuyaWrite(buf, attr)) {
return; // error
}
} else if (!ZbAppendWriteBuf(buf, attr, packet.cmd == ZCL_READ_ATTRIBUTES_RESPONSE)) { // general case