diff --git a/tasmota/support_light_list.ino b/tasmota/support_light_list.ino new file mode 100644 index 000000000..28d2af595 --- /dev/null +++ b/tasmota/support_light_list.ino @@ -0,0 +1,186 @@ +/* + support_light_list.ino - Lightweight Linked List for simple objects - optimized for low code size and low memory + + Copyright (C) 2020 Theo Arends and Stephan Hadinger + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +/*********************************************************************************************\ + * + * private class for Linked List element + * +\*********************************************************************************************/ +template +class LList; + +template +class LList_elt { +public: + + LList_elt() : _next(nullptr), _val() {} + + inline T & val(void) { return _val; } + inline LList_elt * next(void) { return _next; } + inline void next(LList_elt * next) { _next = next; } + + friend class LList; + +protected: + LList_elt * _next; + T _val; +}; + +/*********************************************************************************************\ + * + * Lightweight Linked List - optimized for low code size + * +\*********************************************************************************************/ +template +class LList { +public: + LList() : _head(nullptr) {} + ~LList() { reset(); } + + // remove elements + void removeHead(void); // remove first element + void reset(void); // remove all elements + void remove(const T * val); + + // read the list + inline bool isEmpty(void) const { return (_head == nullptr) ? true : false; } + size_t length(void) const; + inline T * head(void) { return _head ? &_head->_val : nullptr; } + inline const T * head(void) const { return _head ? &_head->_val : nullptr; } + const T * at(size_t index) const ; + // non-const variants + // not very academic cast but reduces code size + inline T * at(size_t index) { return (T*) ((const LList*)this)->at(index); } + + // adding elements + T & addHead(void); + T & addHead(const T &val); + T & addToLast(void); + + // iterator + // see https://stackoverflow.com/questions/8164567/how-to-make-my-custom-type-to-work-with-range-based-for-loops + class iterator { + public: + iterator(LList_elt *_cur): cur(_cur), next(nullptr) { if (cur) { next = cur->_next; } } + iterator operator++() { cur = next; if (cur) { next = cur->_next;} return *this; } + bool operator!=(const iterator & other) const { return cur != other.cur; } + T & operator*() const { return cur->_val; } + private: + LList_elt *cur; + LList_elt *next; // we need to keep next pointer in case the current attribute gets deleted + }; + iterator begin() { return iterator(this->_head); } // start with 'head' + iterator end() { return iterator(nullptr); } // end with null pointer + + // const iterator + class const_iterator { + public: + const_iterator(const LList_elt *_cur): cur(_cur), next(nullptr) { if (cur) { next = cur->_next; } } + const_iterator operator++() { cur = next; if (cur) { next = cur->_next;} return *this; } + bool operator!=(const_iterator & other) const { return cur != other.cur; } + const T & operator*() const { return cur->_val; } + private: + const LList_elt *cur; + const LList_elt *next; // we need to keep next pointer in case the current attribute gets deleted + }; + const_iterator begin() const { return const_iterator(this->_head); } // start with 'head' + const_iterator end() const { return const_iterator(nullptr); } // end with null pointer + +protected: + LList_elt * _head; +}; + +template +size_t LList::length(void) const { + size_t count = 0; + for (auto & elt : *this) {count++; } + return count; +} + + +template +const T * LList::at(size_t index) const { + size_t count = 0; + for (const auto & elt : *this) { + if (index == count++) { return &elt; } + } + return nullptr; +} + +template +void LList::reset(void) { + while (_head) { + LList_elt * next = _head->next(); + delete _head; + _head = next; + } +} + +template +void LList::removeHead(void) { + if (_head) { + LList_elt * next = _head->next(); + delete _head; + _head = next; + } +} + +template +void LList::remove(const T * val) { + if (nullptr == val) { return; } + // find element in chain and find pointer before + LList_elt **curr_ptr = &_head; + while (*curr_ptr) { + LList_elt * curr_elt = *curr_ptr; + if ( &(curr_elt->_val) == val) { + *curr_ptr = curr_elt->_next; // update previous pointer to next element + delete curr_elt; + break; // stop iteration now + } + curr_ptr = &((*curr_ptr)->_next); // move to next element + } +} + +template +T & LList::addHead(void) { + LList_elt * elt = new LList_elt(); // create element + elt->next(_head); // insert at the head + _head = elt; + return elt->_val; +} + +template +T & LList::addHead(const T &val) { + LList_elt * elt = new LList_elt(); // create element + elt->next(_head); // insert at the head + elt->_val = val; + _head = elt; + return elt->_val; +} + +template +T & LList::addToLast(void) { + LList_elt **curr_ptr = &_head; + while (*curr_ptr) { + curr_ptr = &((*curr_ptr)->_next); + } + LList_elt * elt = new LList_elt(); // create element + *curr_ptr = elt; + return elt->_val; +} \ No newline at end of file diff --git a/tasmota/support_static_buffer.ino b/tasmota/support_static_buffer.ino index ea21b2805..81deb083b 100644 --- a/tasmota/support_static_buffer.ino +++ b/tasmota/support_static_buffer.ino @@ -237,3 +237,18 @@ public: _buf = nullptr; } } PreAllocatedSBuffer; + +// nullptr accepted +bool equalsSBuffer(const class SBuffer * buf1, const class SBuffer * buf2) { + if (buf1 == buf2) { return true; } + if (!buf1 && (buf2->len() == 0)) { return true; } + if (!buf2 && (buf1->len() == 0)) { return true; } + if (!buf1 || !buf2) { return false; } // at least one buf is not empty + // we know that both buf1 and buf2 are non-null + if (buf1->len() != buf2->len()) { return false; } + size_t len = buf1->len(); + for (uint32_t i=0; iget8(i) != buf2->get8(i)) { return false; } + } + return true; +} \ No newline at end of file diff --git a/tasmota/xdrv_23_zigbee_1z_libs.ino b/tasmota/xdrv_23_zigbee_1z_libs.ino new file mode 100644 index 000000000..0590cff81 --- /dev/null +++ b/tasmota/xdrv_23_zigbee_1z_libs.ino @@ -0,0 +1,752 @@ +/* + xdrv_23_zigbee_1z_libs.ino - zigbee support for Tasmota, JSON replacement libs + + Copyright (C) 2020 Theo Arends and Stephan Hadinger + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifdef USE_ZIGBEE + +/*********************************************************************************************\ + * Replacement libs for JSON to output a list of attributes +\*********************************************************************************************/ + +// simplified version of strcmp accepting both arguments to be in PMEM, and accepting nullptr arguments +// inspired from https://code.woboq.org/userspace/glibc/string/strcmp.c.html +int strcmp_PP(const char *p1, const char *p2) { + if (p1 == p2) { return 0; } // equality + if (!p1) { return -1; } // first string is null + if (!p2) { return 1; } // second string is null + const unsigned char *s1 = (const unsigned char *) p1; + const unsigned char *s2 = (const unsigned char *) p2; + unsigned char c1, c2; + do { + c1 = (unsigned char) pgm_read_byte(s1); + s1++; + c2 = (unsigned char) pgm_read_byte(s2); + s2++; + if (c1 == '\0') + return c1 - c2; + } + while (c1 == c2); + return c1 - c2; +} + +/*********************************************************************************************\ + * + * Variables for Rules from last Zigbee message received + * +\*********************************************************************************************/ + +typedef struct Z_LastMessageVars { + uint16_t device; // device short address + uint16_t groupaddr; // group address + uint16_t cluster; // cluster id + uint8_t endpoint; // source endpoint +} Z_LastMessageVars; + +Z_LastMessageVars gZbLastMessage; + +uint16_t Z_GetLastDevice(void) { return gZbLastMessage.device; } +uint16_t Z_GetLastGroup(void) { return gZbLastMessage.groupaddr; } +uint16_t Z_GetLastCluster(void) { return gZbLastMessage.cluster; } +uint8_t Z_GetLastEndpoint(void) { return gZbLastMessage.endpoint; } + +/*********************************************************************************************\ + * + * Class for single attribute + * +\*********************************************************************************************/ + +enum class Za_type : uint8_t { + Za_none, // empty, translates into null in JSON + // numericals + Za_bool, // boolean, translates to true/false, uses uval32 to store + Za_uint, // unsigned 32 int, uses uval32 + Za_int, // signed 32 int, uses ival32 + Za_float, // float 32, uses fval + // non-nummericals + Za_raw, // bytes buffer, uses bval + Za_str // string, uses sval +}; + +class Z_attribute { +public: + + // attribute key, either cluster+attribute_id or plain name + union { + struct { + uint16_t cluster; + uint16_t attr_id; + } id; + char * key; + } key; + // attribute value + union { + uint32_t uval32; + int32_t ival32; + float fval; + SBuffer* bval; + char* sval; + } val; + Za_type type; // uint8_t in size, type of attribute, see above + bool key_is_str; // is the key a string? + bool key_is_pmem; // is the string in progmem, so we don't need to make a copy + bool val_str_raw; // if val is String, it is raw JSON and should not be escaped + uint8_t key_suffix; // append a suffix to key (default is 1, explicitly output if >1) + + // Constructor with all defaults + Z_attribute(): + key{ .id = { 0x0000, 0x0000 } }, + val{ .uval32 = 0x0000 }, + type(Za_type::Za_none), + key_is_str(false), + key_is_pmem(false), + val_str_raw(false), + key_suffix(1) + {}; + + Z_attribute(const Z_attribute & rhs) { + deepCopy(rhs); + } + + Z_attribute & operator = (const Z_attribute & rhs) { + freeKey(); + freeVal(); + deepCopy(rhs); + } + + // Destructor, free memory that was allocated + ~Z_attribute() { + freeKey(); + freeVal(); + } + + // free any allocated memoruy for values + void freeVal(void) { + switch (type) { + case Za_type::Za_raw: + if (val.bval) { delete val.bval; val.bval = nullptr; } + break; + case Za_type::Za_str: + if (val.sval) { delete[] val.sval; val.sval = nullptr; } + break; + } + } + // free any allocated memoruy for keys + void freeKey(void) { + if (key_is_str && key.key && !key_is_pmem) { delete[] key.key; } + key.key = nullptr; + } + + // set key name + void setKeyName(const char * _key, bool pmem = false) { + freeKey(); + key_is_str = true; + key_is_pmem = pmem; + if (pmem) { + key.key = (char*) _key; + } else { + setKeyName(_key, nullptr); + } + } + // provide two entries and concat + void setKeyName(const char * _key, const char * _key2) { + freeKey(); + key_is_str = true; + key_is_pmem = false; + if (_key) { + size_t key_len = strlen_P(_key); + if (_key2) { + key_len += strlen_P(_key2); + } + key.key = new char[key_len+1]; + strcpy_P(key.key, _key); + if (_key2) { + strcat_P(key.key, _key2); + } + } + } + + void setKeyId(uint16_t cluster, uint16_t attr_id) { + freeKey(); + key_is_str = false; + key.id.cluster = cluster; + key.id.attr_id = attr_id; + } + + // Setters + void setNone(void) { + freeVal(); // free any previously allocated memory + val.uval32 = 0; + type = Za_type::Za_none; + } + void setUInt(uint32_t _val) { + freeVal(); // free any previously allocated memory + val.uval32 = _val; + type = Za_type::Za_uint; + } + void setBool(bool _val) { + freeVal(); // free any previously allocated memory + val.uval32 = _val; + type = Za_type::Za_bool; + } + void setInt(int32_t _val) { + freeVal(); // free any previously allocated memory + val.ival32 = _val; + type = Za_type::Za_int; + } + void setFloat(float _val) { + freeVal(); // free any previously allocated memory + val.fval = _val; + type = Za_type::Za_float; + } + + void setBuf(const SBuffer &buf, size_t index, size_t len) { + freeVal(); + if (len) { + val.bval = new SBuffer(len); + val.bval->addBuffer(buf.buf(index), len); + } + type = Za_type::Za_raw; + } + + // set the string value + // PMEM argument is allowed + // string will be copied, so it can be changed later + // nullptr is allowed and considered as empty string + // Note: memory is allocated only if string is non-empty + void setStr(const char * _val) { + freeVal(); // free any previously allocated memory + val_str_raw = false; + // val.sval is always nullptr after freeVal() + if (_val) { + size_t len = strlen_P(_val); + if (len) { + val.sval = new char[len+1]; + strcpy_P(val.sval, _val); + } + } + type = Za_type::Za_str; + } + inline void setStrRaw(const char * _val) { + setStr(_val); + val_str_raw = true; + } + + inline bool isNum(void) const { return (type >= Za_type::Za_bool) && (type <= Za_type::Za_float); } + // get num values + float getFloat(void) const { + switch (type) { + case Za_type::Za_bool: + case Za_type::Za_uint: return (float) val.uval32; + case Za_type::Za_int: return (float) val.ival32; + case Za_type::Za_float: return val.fval; + default: return 0.0f; + } + } + + int32_t getInt(void) const { + switch (type) { + case Za_type::Za_bool: + case Za_type::Za_uint: return (int32_t) val.uval32; + case Za_type::Za_int: return val.ival32; + case Za_type::Za_float: return (int32_t) val.fval; + default: return 0; + } + } + + uint32_t getUInt(void) const { + switch (type) { + case Za_type::Za_bool: + case Za_type::Za_uint: return val.uval32; + case Za_type::Za_int: return (uint32_t) val.ival32; + case Za_type::Za_float: return (uint32_t) val.fval; + default: return 0; + } + } + + bool getBool(void) const { + switch (type) { + case Za_type::Za_bool: + case Za_type::Za_uint: return val.uval32 ? true : false; + case Za_type::Za_int: return val.ival32 ? true : false; + case Za_type::Za_float: return val.fval ? true : false; + default: return false; + } + } + + const SBuffer * getRaw(void) const { + if (Za_type::Za_raw == type) { return val.bval; } + return nullptr; + } + + // always return a point to a string, if not defined then empty string. + // Never returns nullptr + const char * getStr(void) const { + if (Za_type::Za_str == type) { return val.sval; } + return ""; + } + + bool equalsKey(const Z_attribute & attr2, bool ignore_key_suffix = false) const { + // check if keys are equal + if (key_is_str != attr2.key_is_str) { return false; } + if (key_is_str) { + if (strcmp_PP(key.key, attr2.key.key)) { return false; } + } else { + if ((key.id.cluster != attr2.key.id.cluster) || + (key.id.attr_id != attr2.key.id.attr_id)) { return false; } + } + if (!ignore_key_suffix) { + if (key_suffix != attr2.key_suffix) { return false; } + } + return true; + } + + bool equalsKey(uint16_t cluster, uint16_t attr_id, uint8_t suffix = 0) const { + if (!key_is_str) { + if ((key.id.cluster == cluster) && (key.id.attr_id == attr_id)) { + if (suffix) { + if (key_suffix == suffix) { return true; } + } else { + return true; + } + } + } + return false; + } + + bool equalsKey(const char * name, uint8_t suffix = 0) const { + if (key_is_str) { + if (0 == strcmp_PP(key.key, name)) { + if (suffix) { + if (key_suffix == suffix) { return true; } + } else { + return true; + } + } + } + return false; + } + + bool equalsVal(const Z_attribute & attr2) const { + if (type != attr2.type) { return false; } + if ((type >= Za_type::Za_bool) && (type <= Za_type::Za_float)) { + // numerical value + if (val.uval32 != attr2.val.uval32) { return false; } + } else if (type == Za_type::Za_raw) { + // compare 2 Static buffers + return equalsSBuffer(val.bval, attr2.val.bval); + } else if (type == Za_type::Za_str) { + // if (val_str_raw != attr2.val_str_raw) { return false; } + if (strcmp_PP(val.sval, attr2.val.sval)) { return false; } + } + return true; + } + + bool equals(const Z_attribute & attr2) const { + return equalsKey(attr2) && equalsVal(attr2); + } + + String toString(bool prefix_comma = false) const { + String res(""); + if (prefix_comma) { res += ','; } + res += '"'; + // compute the attribute name + if (key_is_str) { + if (key.key) { res += EscapeJSONString(key.key); } + else { res += F("null"); } // shouldn't happen + if (key_suffix > 1) { + res += key_suffix; + } + } else { + char attr_name[12]; + snprintf_P(attr_name, sizeof(attr_name), PSTR("%04X/%04X"), key.id.cluster, key.id.attr_id); + res += attr_name; + if (key_suffix > 1) { + res += '+'; + res += key_suffix; + } + } + res += F("\":"); + // value part + switch (type) { + case Za_type::Za_none: + res += "null"; + break; + case Za_type::Za_bool: + res += val.uval32 ? F("true") : F("false"); + break; + case Za_type::Za_uint: + res += val.uval32; + break; + case Za_type::Za_int: + res += val.ival32; + break; + case Za_type::Za_float: + { + String fstr(val.fval, 2); + size_t last = fstr.length() - 1; + // remove trailing zeros + while (fstr[last] == '0') { + fstr.remove(last--); + } + // remove trailing dot + if (fstr[last] == '.') { + fstr.remove(last); + } + res += fstr; + } + break; + case Za_type::Za_raw: + res += '"'; + if (val.bval) { + size_t blen = val.bval->len(); + // print as HEX + char hex[2*blen+1]; + ToHex_P(val.bval->getBuffer(), blen, hex, sizeof(hex)); + res += hex; + } + res += '"'; + break; + case Za_type::Za_str: + if (val_str_raw) { + if (val.sval) { res += val.sval; } + } else { + res += '"'; + if (val.sval) { + res += EscapeJSONString(val.sval); // escape JSON chars + } + res += '"'; + } + break; + } + + return res; + } + + // copy value from one attribute to another, without changing its type + void copyVal(const Z_attribute & rhs) { + freeVal(); + // copy value + val.uval32 = 0x00000000; + type = rhs.type; + if (rhs.isNum()) { + val.uval32 = rhs.val.uval32; + } else if (rhs.type == Za_type::Za_raw) { + if (rhs.val.bval) { + val.bval = new SBuffer(rhs.val.bval->len()); + val.bval->addBuffer(*(rhs.val.bval)); + } + } else if (rhs.type == Za_type::Za_str) { + if (rhs.val.sval) { + size_t s_len = strlen_P(rhs.val.sval); + val.sval = new char[s_len+1]; + strcpy_P(val.sval, rhs.val.sval); + } + } + val_str_raw = rhs.val_str_raw; + } + +protected: + void deepCopy(const Z_attribute & rhs) { + // copy key + if (!rhs.key_is_str) { + key.id.cluster = rhs.key.id.cluster; + key.id.attr_id = rhs.key.id.attr_id; + } else { + if (rhs.key_is_pmem) { + key.key = rhs.key.key; // PMEM, don't copy + } else { + key.key = nullptr; + if (rhs.key.key) { + size_t key_len = strlen_P(rhs.key.key); + if (key_len) { + key.key = new char[key_len+1]; + strcpy_P(key.key, rhs.key.key); + } + } + } + } + key_is_str = rhs.key_is_str; + key_is_pmem = rhs.key_is_pmem; + key_suffix = rhs.key_suffix; + // copy value + copyVal(rhs); + // don't touch next pointer + } +}; + +/*********************************************************************************************\ + * + * Class for attribute array of values + * This is a helper function to generate a clean list of unsigned ints + * +\*********************************************************************************************/ + +class Z_json_array { +public: + + Z_json_array(): val("[]") {} // start with empty array + void add(uint32_t uval32) { + // remove trailing ']' + val.remove(val.length()-1); + if (val.length() > 1) { // if not empty, prefix with comma + val += ','; + } + val += uval32; + val += ']'; + } + String &toString(void) { + return val; + } + +private : + String val; +}; + +/*********************************************************************************************\ + * + * Class for attribute ordered list + * +\*********************************************************************************************/ + + +// Attribute list +// Contains meta-information: +// - source endpoint (is conflicting) +// - LQI (if not conflicting) +class Z_attribute_list : public LList { +public: + uint8_t src_ep; // source endpoint, 0xFF if unknown + uint8_t lqi; // linkquality, 0xFF if unknown + uint16_t group_id; // group address OxFFFF if inknown + + Z_attribute_list(): + LList(), // call superclass constructor + src_ep(0xFF), + lqi(0xFF), + group_id(0xFFFF) + {}; + + // we don't define any destructor, the superclass destructor is automatically called + + // reset object to its initial state + // free all allocated memory + void reset(void) { + LList::reset(); + src_ep = 0xFF; + lqi = 0xFF; + group_id = 0xFFFF; + } + + inline bool isValidSrcEp(void) const { return 0xFF != src_ep; } + inline bool isValidLQI(void) const { return 0xFF != lqi; } + inline bool isValidGroupId(void) const { return 0xFFFF != group_id; } + + // the following addAttribute() compute the suffix and increments it + // Add attribute to the list, given cluster and attribute id + Z_attribute & addAttribute(uint16_t cluster, uint16_t attr_id, uint8_t suffix = 0); + + // Add attribute to the list, given name + Z_attribute & addAttribute(const char * name, bool pmem = false, uint8_t suffix = 0); + Z_attribute & addAttribute(const char * name, const char * name2, uint8_t suffix = 0); + inline Z_attribute & addAttribute(const __FlashStringHelper * name, uint8_t suffix = 0) { + return addAttribute((const char*) name, true, suffix); + } + + // Remove from list by reference, if null or not found, then do nothing + inline void removeAttribute(const Z_attribute * attr) { remove(attr); } + + // dump the entire structure as JSON, starting from head (as parameter) + // does not start not end with a comma + // do we enclosed in brackets '{' '}' + String toString(bool enclose_brackets = false) const; + + // find if attribute with same key already exists, return null if not found + const Z_attribute * findAttribute(uint16_t cluster, uint16_t attr_id, uint8_t suffix = 0) const; + const Z_attribute * findAttribute(const char * name, uint8_t suffix = 0) const; + const Z_attribute * findAttribute(const Z_attribute &attr) const; // suffis always count here + // non-const variants + inline Z_attribute * findAttribute(uint16_t cluster, uint16_t attr_id, uint8_t suffix = 0) { + return (Z_attribute*) ((const Z_attribute_list*)this)->findAttribute(cluster, attr_id, suffix); + } + inline Z_attribute * findAttribute(const char * name, uint8_t suffix = 0) { + return (Z_attribute*) (((const Z_attribute_list*)this)->findAttribute(name, suffix)); + } + inline Z_attribute * findAttribute(const Z_attribute &attr) { + return (Z_attribute*) ((const Z_attribute_list*)this)->findAttribute(attr); + } + + // count matching attributes, ignoring suffix + size_t countAttribute(uint16_t cluster, uint16_t attr_id) const ; + size_t countAttribute(const char * name) const ; + + // if suffix == 0, we don't care and find the first match + Z_attribute & findOrCreateAttribute(uint16_t cluster, uint16_t attr_id, uint8_t suffix = 0); + Z_attribute & findOrCreateAttribute(const char * name, uint8_t suffix = 0); + // always care about suffix + Z_attribute & findOrCreateAttribute(const Z_attribute &attr); + // replace attribute with new value, suffix does care + Z_attribute & replaceOrCreate(const Z_attribute &attr); + + // merge with secondary list, return true if ok, false if conflict + bool mergeList(const Z_attribute_list &list2); +}; + +// add a cluster/attr_id attribute at the end of the list +Z_attribute & Z_attribute_list::addAttribute(uint16_t cluster, uint16_t attr_id, uint8_t suffix) { + Z_attribute & attr = addToLast(); + attr.key.id.cluster = cluster; + attr.key.id.attr_id = attr_id; + attr.key_is_str = false; + if (!suffix) { attr.key_suffix = countAttribute(attr.key.id.cluster, attr.key.id.attr_id); } + else { attr.key_suffix = suffix; } + return attr; +} + +// add a cluster/attr_id attribute at the end of the list +Z_attribute & Z_attribute_list::addAttribute(const char * name, bool pmem, uint8_t suffix) { + Z_attribute & attr = addToLast(); + attr.setKeyName(name, pmem); + if (!suffix) { attr.key_suffix = countAttribute(attr.key.key); } + else { attr.key_suffix = suffix; } + return attr; +} + +Z_attribute & Z_attribute_list::addAttribute(const char * name, const char * name2, uint8_t suffix) { + Z_attribute & attr = addToLast(); + attr.setKeyName(name, name2); + if (!suffix) { attr.key_suffix = countAttribute(attr.key.key); } + else { attr.key_suffix = suffix; } + return attr; +} + +String Z_attribute_list::toString(bool enclose_brackets) const { + String res = ""; + if (enclose_brackets) { res += '{'; } + bool prefix_comma = false; + for (const auto & attr : *this) { + res += attr.toString(prefix_comma); + prefix_comma = true; + } + // add source endpoint + if (0xFF != src_ep) { + if (prefix_comma) { res += ','; } + prefix_comma = true; + res += F("\"" D_CMND_ZIGBEE_ENDPOINT "\":"); + res += src_ep; + } + // add group address + if (0xFFFF != group_id) { + if (prefix_comma) { res += ','; } + prefix_comma = true; + res += F("\"" D_CMND_ZIGBEE_GROUP "\":"); + res += group_id; + } + // add lqi + if (0xFF != lqi) { + if (prefix_comma) { res += ','; } + prefix_comma = true; + res += F("\"" D_CMND_ZIGBEE_LINKQUALITY "\":"); + res += lqi; + } + if (enclose_brackets) { res += '}'; } + // done + return res; +} + +// suffis always count here +const Z_attribute * Z_attribute_list::findAttribute(const Z_attribute &attr) const { + uint8_t suffix = attr.key_suffix; + if (attr.key_is_str) { + return findAttribute(attr.key.key, suffix); + } else { + return findAttribute(attr.key.id.cluster, attr.key.id.attr_id, suffix); + } +} + +const Z_attribute * Z_attribute_list::findAttribute(uint16_t cluster, uint16_t attr_id, uint8_t suffix) const { + for (const auto & attr : *this) { + if (attr.equalsKey(cluster, attr_id, suffix)) { return &attr; } + } + return nullptr; +} +size_t Z_attribute_list::countAttribute(uint16_t cluster, uint16_t attr_id) const { + size_t count = 0; + for (const auto & attr : *this) { + if (attr.equalsKey(cluster, attr_id, 0)) { count++; } + } + return count; +} + +// return the existing attribute or create a new one +Z_attribute & Z_attribute_list::findOrCreateAttribute(uint16_t cluster, uint16_t attr_id, uint8_t suffix) { + Z_attribute * found = findAttribute(cluster, attr_id, suffix); + return found ? *found : addAttribute(cluster, attr_id, suffix); +} + +const Z_attribute * Z_attribute_list::findAttribute(const char * name, uint8_t suffix) const { + for (const auto & attr : *this) { + if (attr.equalsKey(name, suffix)) { return &attr; } + } + return nullptr; +} +size_t Z_attribute_list::countAttribute(const char * name) const { + size_t count = 0; + for (const auto & attr : *this) { + if (attr.equalsKey(name, 0)) { count++; } + } + return count; +} +// return the existing attribute or create a new one +Z_attribute & Z_attribute_list::findOrCreateAttribute(const char * name, uint8_t suffix) { + Z_attribute * found = findAttribute(name, suffix); + return found ? *found : addAttribute(name, suffix); +} + +// same but passing a Z_attribute as key +Z_attribute & Z_attribute_list::findOrCreateAttribute(const Z_attribute &attr) { + if (attr.key_is_str) { + return findOrCreateAttribute(attr.key.key, attr.key_suffix); + } else { + return findOrCreateAttribute(attr.key.id.cluster, attr.key.id.attr_id, attr.key_suffix); + } +} +// replace the entire content with new attribute or create +Z_attribute & Z_attribute_list::replaceOrCreate(const Z_attribute &attr) { + Z_attribute &new_attr = findOrCreateAttribute(attr); + new_attr.copyVal(attr); + return new_attr; +} + + +bool Z_attribute_list::mergeList(const Z_attribute_list &attr_list) { + // Check source endpoint + if (0xFF == src_ep) { + src_ep = attr_list.src_ep; + } else if (0xFF != attr_list.src_ep) { + if (src_ep != attr_list.src_ep) { return false; } + } + if (0xFF != attr_list.lqi) { + lqi = attr_list.lqi; + } + for (auto & attr : attr_list) { + replaceOrCreate(attr); + } + return true; +} + +#endif // USE_ZIGBEE diff --git a/tasmota/xdrv_23_zigbee_2_devices.ino b/tasmota/xdrv_23_zigbee_2_devices.ino index aa7ea9d2c..8ad36f861 100644 --- a/tasmota/xdrv_23_zigbee_2_devices.ino +++ b/tasmota/xdrv_23_zigbee_2_devices.ino @@ -19,8 +19,6 @@ #ifdef USE_ZIGBEE -#include - #ifndef ZIGBEE_SAVE_DELAY_SECONDS #define ZIGBEE_SAVE_DELAY_SECONDS 2 // wait for 2s before saving Zigbee info #endif @@ -29,25 +27,6 @@ const uint16_t kZigbeeSaveDelaySeconds = ZIGBEE_SAVE_DELAY_SECONDS; // wait f /*********************************************************************************************\ * Structures for Rules variables related to the last received message \*********************************************************************************************/ - -typedef struct Z_LastMessageVars { - uint16_t device; // device short address - uint16_t groupaddr; // group address - uint16_t cluster; // cluster id - uint8_t endpoint; // source endpoint -} Z_LastMessageVars; - -Z_LastMessageVars gZbLastMessage; - -uint16_t Z_GetLastDevice(void) { return gZbLastMessage.device; } -uint16_t Z_GetLastGroup(void) { return gZbLastMessage.groupaddr; } -uint16_t Z_GetLastCluster(void) { return gZbLastMessage.cluster; } -uint8_t Z_GetLastEndpoint(void) { return gZbLastMessage.endpoint; } - -/*********************************************************************************************\ - * Structures for device configuration -\*********************************************************************************************/ - const size_t endpoints_max = 8; // we limit to 8 endpoints class Z_Device { @@ -58,9 +37,8 @@ public: char * modelId; char * friendlyName; uint8_t endpoints[endpoints_max]; // static array to limit memory consumption, list of endpoints until 0x00 or end of array - // json buffer used for attribute reporting - DynamicJsonBuffer *json_buffer; - JsonObject *json; + // Used for attribute reporting + Z_attribute_list attr_list; // sequence number for Zigbee frames uint16_t shortaddr; // unique key if not null, or unspecified if null uint8_t seqNumber; @@ -95,14 +73,13 @@ public: int16_t mains_power; // Active power // Constructor with all defaults - Z_Device(uint16_t _shortaddr, uint64_t _longaddr = 0x00): + Z_Device(uint16_t _shortaddr = BAD_SHORTADDR, uint64_t _longaddr = 0x00): longaddr(_longaddr), manufacturerId(nullptr), modelId(nullptr), friendlyName(nullptr), endpoints{ 0, 0, 0, 0, 0, 0, 0, 0 }, - json_buffer(nullptr), - json(nullptr), + attr_list(), shortaddr(_shortaddr), seqNumber(0), // Hue support @@ -207,7 +184,7 @@ typedef struct Z_Deferred { // - shortaddr and longaddr cannot be both null class Z_Devices { public: - Z_Devices() {}; + Z_Devices() : _deferred() {}; // Probe the existence of device keys // Results: @@ -218,12 +195,17 @@ public: uint16_t isKnownIndex(uint32_t index) const; uint16_t isKnownFriendlyName(const char * name) const; + Z_Device & findShortAddr(uint16_t shortaddr); const Z_Device & findShortAddr(uint16_t shortaddr) const; + Z_Device & findLongAddr(uint64_t longaddr); + const Z_Device & findLongAddr(uint64_t longaddr) const; Z_Device & getShortAddr(uint16_t shortaddr); // find Device from shortAddr, creates it if does not exist - const Z_Device & getShortAddrConst(uint16_t shortaddr) const ; // find Device from shortAddr, creates it if does not exist Z_Device & getLongAddr(uint64_t longaddr); // find Device from shortAddr, creates it if does not exist + // check if a device was found or if it's the fallback device + inline bool foundDevice(const Z_Device & device) const { + return (&device != &device_unk); + } - int32_t findLongAddr(uint64_t longaddr) const; int32_t findFriendlyName(const char * name) const; uint64_t getDeviceLongAddr(uint16_t shortaddr) const; @@ -281,19 +263,22 @@ public: void runTimer(void); // Append or clear attributes Json structure - void jsonClear(uint16_t shortaddr); - void jsonAppend(uint16_t shortaddr, const JsonObject &values); - const JsonObject *jsonGet(uint16_t shortaddr); + void jsonAppend(uint16_t shortaddr, const Z_attribute_list &attr_list); void jsonPublishFlush(uint16_t shortaddr); // publish the json message and clear buffer - bool jsonIsConflict(uint16_t shortaddr, const JsonObject &values); - void jsonPublishNow(uint16_t shortaddr, JsonObject &values); + bool jsonIsConflict(uint16_t shortaddr, const Z_attribute_list &attr_list) const; + void jsonPublishNow(uint16_t shortaddr, Z_attribute_list &attr_list); // Iterator size_t devicesSize(void) const { - return _devices.size(); + return _devices.length(); } - const Z_Device &devicesAt(size_t i) const { - return *(_devices.at(i)); + const Z_Device & devicesAt(size_t i) const { + const Z_Device * devp = _devices.at(i); + if (devp) { + return *devp; + } else { + return device_unk; + } } // Remove device from list @@ -308,8 +293,8 @@ public: uint16_t parseDeviceParam(const char * param, bool short_must_be_known = false) const; private: - std::vector _devices = {}; - std::vector _deferred = {}; // list of deferred calls + LList _devices; // list of devices + LList _deferred; // list of deferred calls uint32_t _saveTimer = 0; uint8_t _seqNumber = 0; // global seqNumber if device is unknown @@ -317,10 +302,7 @@ private: // Any find() function will not return Null, instead it will return this instance const Z_Device device_unk = Z_Device(BAD_SHORTADDR); - template < typename T> - static bool findInVector(const std::vector & vecOfElements, const T & element); - - int32_t findShortAddrIdx(uint16_t shortaddr) const; + //int32_t findShortAddrIdx(uint16_t shortaddr) const; // Create a new entry in the devices list - must be called if it is sure it does not already exist Z_Device & createDeviceEntry(uint16_t shortaddr, uint64_t longaddr = 0); void freeDeviceEntry(Z_Device *device); @@ -343,31 +325,16 @@ uint16_t localShortAddr = 0; * Implementation \*********************************************************************************************/ -// https://thispointer.com/c-how-to-find-an-element-in-vector-and-get-its-index/ -template < typename T> -bool Z_Devices::findInVector(const std::vector & vecOfElements, const T & element) { - // Find given element in vector - auto it = std::find(vecOfElements.begin(), vecOfElements.end(), element); - - if (it != vecOfElements.end()) { - return true; - } else { - return false; - } -} - // // Create a new Z_Device entry in _devices. Only to be called if you are sure that no // entry with same shortaddr or longaddr exists. // Z_Device & Z_Devices::createDeviceEntry(uint16_t shortaddr, uint64_t longaddr) { if ((BAD_SHORTADDR == shortaddr) && !longaddr) { return (Z_Device&) device_unk; } // it is not legal to create this entry - Z_Device * device_alloc = new Z_Device(shortaddr, longaddr); + Z_Device device(shortaddr, longaddr); - device_alloc->json_buffer = new DynamicJsonBuffer(16); - _devices.push_back(device_alloc); dirty(); - return *(_devices.back()); + return _devices.addHead(device); } void Z_Devices::freeDeviceEntry(Z_Device *device) { @@ -383,20 +350,17 @@ void Z_Devices::freeDeviceEntry(Z_Device *device) { // In: // shortaddr (not BAD_SHORTADDR) // Out: -// index in _devices of entry, -1 if not found -// -int32_t Z_Devices::findShortAddrIdx(uint16_t shortaddr) const { - if (BAD_SHORTADDR == shortaddr) { return -1; } // does not make sense to look for BAD_SHORTADDR shortaddr (broadcast) - int32_t found = 0; - for (auto &elem : _devices) { - if (elem->shortaddr == shortaddr) { return found; } - found++; +// reference to device, or to device_unk if not found +// (use foundDevice() to check if found) +Z_Device & Z_Devices::findShortAddr(uint16_t shortaddr) { + for (auto & elem : _devices) { + if (elem.shortaddr == shortaddr) { return elem; } } - return -1; + return (Z_Device&) device_unk; } const Z_Device & Z_Devices::findShortAddr(uint16_t shortaddr) const { - for (auto &elem : _devices) { - if (elem->shortaddr == shortaddr) { return *elem; } + for (const auto & elem : _devices) { + if (elem.shortaddr == shortaddr) { return elem; } } return device_unk; } @@ -408,14 +372,19 @@ const Z_Device & Z_Devices::findShortAddr(uint16_t shortaddr) const { // Out: // index in _devices of entry, -1 if not found // -int32_t Z_Devices::findLongAddr(uint64_t longaddr) const { - if (!longaddr) { return -1; } - int32_t found = 0; +Z_Device & Z_Devices::findLongAddr(uint64_t longaddr) { + if (!longaddr) { return (Z_Device&) device_unk; } for (auto &elem : _devices) { - if (elem->longaddr == longaddr) { return found; } - found++; + if (elem.longaddr == longaddr) { return elem; } } - return -1; + return (Z_Device&) device_unk; +} +const Z_Device & Z_Devices::findLongAddr(uint64_t longaddr) const { + if (!longaddr) { return device_unk; } + for (const auto &elem : _devices) { + if (elem.longaddr == longaddr) { return elem; } + } + return device_unk; } // // Scan all devices to find a corresponding friendlyNme @@ -431,8 +400,8 @@ int32_t Z_Devices::findFriendlyName(const char * name) const { int32_t found = 0; if (name_len) { for (auto &elem : _devices) { - if (elem->friendlyName) { - if (strcasecmp(elem->friendlyName, name) == 0) { return found; } + if (elem.friendlyName) { + if (strcasecmp(elem.friendlyName, name) == 0) { return found; } } found++; } @@ -441,9 +410,8 @@ int32_t Z_Devices::findFriendlyName(const char * name) const { } uint16_t Z_Devices::isKnownLongAddr(uint64_t longaddr) const { - int32_t found = findLongAddr(longaddr); - if (found >= 0) { - const Z_Device & device = devicesAt(found); + const Z_Device & device = findLongAddr(longaddr); + if (foundDevice(device)) { return device.shortaddr; // can be zero, if not yet registered } else { return BAD_SHORTADDR; @@ -471,7 +439,7 @@ uint16_t Z_Devices::isKnownFriendlyName(const char * name) const { } uint64_t Z_Devices::getDeviceLongAddr(uint16_t shortaddr) const { - return getShortAddrConst(shortaddr).longaddr; // if unknown, it reverts to the Unknown device and longaddr is 0x00 + return findShortAddr(shortaddr).longaddr; // if unknown, it reverts to the Unknown device and longaddr is 0x00 } // @@ -479,38 +447,28 @@ uint64_t Z_Devices::getDeviceLongAddr(uint16_t shortaddr) const { // Z_Device & Z_Devices::getShortAddr(uint16_t shortaddr) { if (BAD_SHORTADDR == shortaddr) { return (Z_Device&) device_unk; } // this is not legal - int32_t found = findShortAddrIdx(shortaddr); - if (found >= 0) { - return *(_devices[found]); + Z_Device & device = findShortAddr(shortaddr); + if (foundDevice(device)) { + return device; } - //Serial.printf("Device entry created for shortaddr = 0x%02X, found = %d\n", shortaddr, found); return createDeviceEntry(shortaddr, 0); } -// Same version but Const -const Z_Device & Z_Devices::getShortAddrConst(uint16_t shortaddr) const { - int32_t found = findShortAddrIdx(shortaddr); - if (found >= 0) { - return *(_devices[found]); - } - return device_unk; -} // find the Device object by its longaddr (unique key if not null) Z_Device & Z_Devices::getLongAddr(uint64_t longaddr) { if (!longaddr) { return (Z_Device&) device_unk; } - int32_t found = findLongAddr(longaddr); - if (found > 0) { - return *(_devices[found]); + Z_Device & device = findLongAddr(longaddr); + if (foundDevice(device)) { + return device; } return createDeviceEntry(0, longaddr); } // Remove device from list, return true if it was known, false if it was not recorded bool Z_Devices::removeDevice(uint16_t shortaddr) { - int32_t found = findShortAddrIdx(shortaddr); - if (found >= 0) { - freeDeviceEntry(_devices.at(found)); - _devices.erase(_devices.begin() + found); + Z_Device & device = findShortAddr(shortaddr); + if (foundDevice(device)) { + _devices.remove(&device); dirty(); return true; } @@ -523,27 +481,27 @@ bool Z_Devices::removeDevice(uint16_t shortaddr) { // shortaddr // longaddr (both can't be null at the same time) void Z_Devices::updateDevice(uint16_t shortaddr, uint64_t longaddr) { - int32_t s_found = findShortAddrIdx(shortaddr); // is there already a shortaddr entry - int32_t l_found = findLongAddr(longaddr); // is there already a longaddr entry + Z_Device * s_found = &findShortAddr(shortaddr); // is there already a shortaddr entry + Z_Device * l_found = &findLongAddr(longaddr); // is there already a longaddr entry - if ((s_found >= 0) && (l_found >= 0)) { // both shortaddr and longaddr are already registered + if (foundDevice(*s_found) && foundDevice(*l_found)) { // both shortaddr and longaddr are already registered if (s_found == l_found) { } else { // they don't match // the device with longaddr got a new shortaddr - _devices[l_found]->shortaddr = shortaddr; // update the shortaddr corresponding to the longaddr + l_found->shortaddr = shortaddr; // update the shortaddr corresponding to the longaddr // erase the previous shortaddr - freeDeviceEntry(_devices.at(s_found)); - _devices.erase(_devices.begin() + s_found); + freeDeviceEntry(s_found); + _devices.remove(s_found); dirty(); } - } else if (s_found >= 0) { + } else if (foundDevice(*s_found)) { // shortaddr already exists but longaddr not // add the longaddr to the entry - _devices[s_found]->longaddr = longaddr; + s_found->longaddr = longaddr; dirty(); - } else if (l_found >= 0) { + } else if (foundDevice(*l_found)) { // longaddr entry exists, update shortaddr - _devices[l_found]->shortaddr = shortaddr; + l_found->shortaddr = shortaddr; dirty(); } else { // neither short/lonf addr are found. @@ -588,9 +546,8 @@ void Z_Devices::addEndpoint(uint16_t shortaddr, uint8_t endpoint) { // uint32_t Z_Devices::countEndpoints(uint16_t shortaddr) const { uint32_t count_ep = 0; - int32_t found = findShortAddrIdx(shortaddr); - if (found < 0) return 0; // avoid creating an entry if the device was never seen - const Z_Device &device = devicesAt(found); + const Z_Device & device =findShortAddr(shortaddr); + if (!foundDevice(device)) return 0; for (uint32_t i = 0; i < endpoints_max; i++) { if (0 != device.endpoints[i]) { @@ -666,9 +623,8 @@ void Z_Devices::setBatteryPercent(uint16_t shortaddr, uint8_t bp) { // get the next sequance number for the device, or use the global seq number if device is unknown uint8_t Z_Devices::getNextSeqNumber(uint16_t shortaddr) { - int32_t short_found = findShortAddrIdx(shortaddr); - if (short_found >= 0) { - Z_Device &device = getShortAddr(shortaddr); + Z_Device & device = findShortAddr(shortaddr); + if (foundDevice(device)) { device.seqNumber += 1; return device.seqNumber; } else { @@ -754,9 +710,9 @@ void Z_Devices::hideHueBulb(uint16_t shortaddr, bool hidden) { } // true if device is not knwon or not a bulb - it wouldn't make sense to publish a non-bulb bool Z_Devices::isHueBulbHidden(uint16_t shortaddr) const { - int32_t found = findShortAddrIdx(shortaddr); - if (found >= 0) { - uint8_t zb_profile = _devices[found]->zb_profile; + const Z_Device & device = findShortAddr(shortaddr); + if (foundDevice(device)) { + uint8_t zb_profile = device.zb_profile; if (0x00 == (zb_profile & 0xF0)) { // bulb type return (zb_profile & 0x08) ? true : false; @@ -769,13 +725,10 @@ bool Z_Devices::isHueBulbHidden(uint16_t shortaddr) const { // Parse for a specific category, of all deferred for a device if category == 0xFF void Z_Devices::resetTimersForDevice(uint16_t shortaddr, uint16_t groupaddr, uint8_t category) { // iterate the list of deferred, and remove any linked to the shortaddr - for (auto it = _deferred.begin(); it != _deferred.end(); it++) { - // Notice that the iterator is decremented after it is passed - // to erase() but before erase() is executed - // see https://www.techiedelight.com/remove-elements-vector-inside-loop-cpp/ - if ((it->shortaddr == shortaddr) && (it->groupaddr == groupaddr)) { - if ((0xFF == category) || (it->category == category)) { - _deferred.erase(it--); + for (auto & defer : _deferred) { + if ((defer.shortaddr == shortaddr) && (defer.groupaddr == groupaddr)) { + if ((0xFF == category) || (defer.category == category)) { + _deferred.remove(&defer); } } } @@ -789,7 +742,8 @@ void Z_Devices::setTimer(uint16_t shortaddr, uint16_t groupaddr, uint32_t wait_m } // Now create the new timer - Z_Deferred deferred = { wait_ms + millis(), // timer + Z_Deferred & deferred = _deferred.addHead(); + deferred = { wait_ms + millis(), // timer shortaddr, groupaddr, cluster, @@ -797,20 +751,17 @@ void Z_Devices::setTimer(uint16_t shortaddr, uint16_t groupaddr, uint32_t wait_m category, value, func }; - _deferred.push_back(deferred); } // Run timer at each tick // WARNING: don't set a new timer within a running timer, this causes memory corruption void Z_Devices::runTimer(void) { // visit all timers - for (auto it = _deferred.begin(); it != _deferred.end(); it++) { - Z_Deferred &defer = *it; - + for (auto & defer : _deferred) { uint32_t timer = defer.timer; if (TimeReached(timer)) { (*defer.func)(defer.shortaddr, defer.groupaddr, defer.cluster, defer.endpoint, defer.value); - _deferred.erase(it--); // remove from list + _deferred.remove(&defer); } } @@ -821,173 +772,100 @@ void Z_Devices::runTimer(void) { } } -// Clear the JSON buffer for coalesced and deferred attributes -void Z_Devices::jsonClear(uint16_t shortaddr) { - Z_Device & device = getShortAddr(shortaddr); - device.json = nullptr; - device.json_buffer->clear(); -} - -// Copy JSON from one object to another, this helps preserving the order of attributes -void CopyJsonVariant(JsonObject &to, const String &key, const JsonVariant &val) { - // first remove the potentially existing key in the target JSON, so new adds will be at the end of the list - to.remove(key); // force remove to have metadata like LinkQuality at the end - - if (val.is()) { - const char * sval = val.as(); // using char* forces a copy, and also captures 'null' values - to.set(key, (char*) sval); - } else if (val.is()) { - JsonArray &nested_arr = to.createNestedArray(key); - CopyJsonArray(nested_arr, val.as()); // deep copy - } else if (val.is()) { - JsonObject &nested_obj = to.createNestedObject(key); - CopyJsonObject(nested_obj, val.as()); // deep copy - } else { - to.set(key, val); // general case for non array, object or string - } -} - -// Shallow copy of array, we skip any sub-array or sub-object. It may be added in the future -void CopyJsonArray(JsonArray &to, const JsonArray &arr) { - for (auto v : arr) { - if (v.is()) { - String sval = v.as(); // force a copy of the String value - to.add(sval); - } else if (v.is()) { - } else if (v.is()) { - } else { - to.add(v); - } - } -} - -// Deep copy of object -void CopyJsonObject(JsonObject &to, const JsonObject &from) { - for (auto kv : from) { - String key_string = kv.key; - JsonVariant &val = kv.value; - - CopyJsonVariant(to, key_string, val); - } -} - // does the new payload conflicts with the existing payload, i.e. values would be overwritten // true - one attribute (except LinkQuality) woudl be lost, there is conflict // false - new attributes can be safely added -bool Z_Devices::jsonIsConflict(uint16_t shortaddr, const JsonObject &values) { - Z_Device & device = getShortAddr(shortaddr); - if (&values == nullptr) { return false; } +bool Z_Devices::jsonIsConflict(uint16_t shortaddr, const Z_attribute_list &attr_list) const { + const Z_Device & device = findShortAddr(shortaddr); - if (nullptr == device.json) { + if (!foundDevice(device)) { return false; } + if (attr_list.isEmpty()) { return false; // if no previous value, no conflict } // compare groups - // Special case for group addresses. Group attribute is only present if the target - // address is a group address, so just comparing attributes will not work. - // Eg: if the first packet has no group attribute, and the second does, conflict would not be detected - // Here we explicitly compute the group address of both messages, and compare them. No group means group=0x0000 - // (we use the property of an missing attribute returning 0) - // (note: we use .get() here which is case-sensitive. We know however that the attribute was set with the exact syntax D_CMND_ZIGBEE_GROUP, so we don't need a case-insensitive get()) - uint16_t group1 = device.json->get(D_CMND_ZIGBEE_GROUP); - uint16_t group2 = values.get(D_CMND_ZIGBEE_GROUP); - if (group1 != group2) { - return true; // if group addresses differ, then conflict + if (device.attr_list.isValidGroupId() && attr_list.isValidGroupId()) { + if (device.attr_list.group_id != attr_list.group_id) { return true; } // groups are in conflict } - // parse all other parameters - for (auto kv : values) { - String key_string = kv.key; + // compare src_ep + if (device.attr_list.isValidSrcEp() && attr_list.isValidSrcEp()) { + if (device.attr_list.src_ep != attr_list.src_ep) { return true; } + } + + // LQI does not count as conflicting - if (0 == strcasecmp_P(kv.key, PSTR(D_CMND_ZIGBEE_GROUP))) { - // ignore group, it was handled already - } else if (0 == strcasecmp_P(kv.key, PSTR(D_CMND_ZIGBEE_ENDPOINT))) { - // attribute "Endpoint" or "Group" - if (device.json->containsKey(kv.key)) { - if (kv.value.as() != device.json->get(kv.key)) { - return true; - } - } - } else if (strcasecmp_P(kv.key, PSTR(D_CMND_ZIGBEE_LINKQUALITY))) { // exception = ignore duplicates for LinkQuality - if (device.json->containsKey(kv.key)) { - return true; // conflict! + // parse all other parameters + for (const auto & attr : attr_list) { + const Z_attribute * curr_attr = device.attr_list.findAttribute(attr); + if (nullptr != curr_attr) { + if (!curr_attr->equalsVal(attr)) { + return true; // the value already exists and is different - conflict! } } } return false; } -void Z_Devices::jsonAppend(uint16_t shortaddr, const JsonObject &values) { +void Z_Devices::jsonAppend(uint16_t shortaddr, const Z_attribute_list &attr_list) { Z_Device & device = getShortAddr(shortaddr); - if (&values == nullptr) { return; } - - if (nullptr == device.json) { - device.json = &(device.json_buffer->createObject()); - } - // Prepend Device, will be removed later if redundant - char sa[8]; - snprintf_P(sa, sizeof(sa), PSTR("0x%04X"), shortaddr); - device.json->set(F(D_JSON_ZIGBEE_DEVICE), sa); - // Prepend Friendly Name if it has one - const char * fname = zigbee_devices.getFriendlyName(shortaddr); - if (fname) { - device.json->set(F(D_JSON_ZIGBEE_NAME), (char*) fname); // (char*) forces ArduinoJson to make a copy of the cstring - } - - // copy all values from 'values' to 'json' - CopyJsonObject(*device.json, values); -} - -const JsonObject *Z_Devices::jsonGet(uint16_t shortaddr) { - return getShortAddr(shortaddr).json; + device.attr_list.mergeList(attr_list); } void Z_Devices::jsonPublishFlush(uint16_t shortaddr) { Z_Device & device = getShortAddr(shortaddr); if (!device.valid()) { return; } // safeguard - JsonObject & json = *device.json; - if (&json == nullptr) { return; } // abort if nothing in buffer + Z_attribute_list &attr_list = device.attr_list; - const char * fname = zigbee_devices.getFriendlyName(shortaddr); - bool use_fname = (Settings.flag4.zigbee_use_names) && (fname); // should we replace shortaddr with friendlyname? + if (!attr_list.isEmpty()) { + const char * fname = zigbee_devices.getFriendlyName(shortaddr); + bool use_fname = (Settings.flag4.zigbee_use_names) && (fname); // should we replace shortaddr with friendlyname? - // save parameters is global variables to be used by Rules - gZbLastMessage.device = shortaddr; // %zbdevice% - gZbLastMessage.groupaddr = json[F(D_CMND_ZIGBEE_GROUP)]; // %zbgroup% - gZbLastMessage.cluster = json[F(D_CMND_ZIGBEE_CLUSTER)]; // %zbcluster% - gZbLastMessage.endpoint = json[F(D_CMND_ZIGBEE_ENDPOINT)]; // %zbendpoint% + // save parameters is global variables to be used by Rules + gZbLastMessage.device = shortaddr; // %zbdevice% + gZbLastMessage.groupaddr = attr_list.group_id; // %zbgroup% + gZbLastMessage.endpoint = attr_list.src_ep; // %zbendpoint% - // dump json in string - String msg = ""; - json.printTo(msg); - zigbee_devices.jsonClear(shortaddr); - - if (use_fname) { - if (Settings.flag4.remove_zbreceived) { - Response_P(PSTR("{\"%s\":%s}"), fname, msg.c_str()); - } else { - Response_P(PSTR("{\"" D_JSON_ZIGBEE_RECEIVED "\":{\"%s\":%s}}"), fname, msg.c_str()); + mqtt_data[0] = 0; // clear string + // Do we prefix with `ZbReceived`? + if (!Settings.flag4.remove_zbreceived) { + Response_P(PSTR("{\"" D_JSON_ZIGBEE_RECEIVED "\":")); } - } else { - if (Settings.flag4.remove_zbreceived) { - Response_P(PSTR("{\"0x%04X\":%s}"), shortaddr, msg.c_str()); + // What key do we use, shortaddr or name? + if (use_fname) { + Response_P(PSTR("%s{\"%s\":{"), mqtt_data, fname); } else { - Response_P(PSTR("{\"" D_JSON_ZIGBEE_RECEIVED "\":{\"0x%04X\":%s}}"), shortaddr, msg.c_str()); + Response_P(PSTR("%s{\"0x%04X\":{"), mqtt_data, shortaddr); } + // Add "Device":"0x...." + Response_P(PSTR("%s\"" D_JSON_ZIGBEE_DEVICE "\":\"0x%04X\","), mqtt_data, shortaddr); + // Add "Name":"xxx" if name is present + if (fname) { + Response_P(PSTR("%s\"" D_JSON_ZIGBEE_NAME "\":\"%s\","), mqtt_data, EscapeJSONString(fname).c_str()); + } + // Add all other attributes + Response_P(PSTR("%s%s}}"), mqtt_data, attr_list.toString().c_str()); + + if (!Settings.flag4.remove_zbreceived) { + Response_P(PSTR("%s}"), mqtt_data); + } + // AddLog_P2(LOG_LEVEL_INFO, PSTR(">>> %s"), mqtt_data); // TODO + attr_list.reset(); // clear the attributes + + if (Settings.flag4.zigbee_distinct_topics) { + char subtopic[16]; + snprintf_P(subtopic, sizeof(subtopic), PSTR("%04X/" D_RSLT_SENSOR), shortaddr); + MqttPublishPrefixTopic_P(TELE, subtopic, Settings.flag.mqtt_sensor_retain); + } else { + MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain); + } + XdrvRulesProcess(); // apply rules } - if (Settings.flag4.zigbee_distinct_topics) { - char subtopic[16]; - snprintf_P(subtopic, sizeof(subtopic), PSTR("%04X/" D_RSLT_SENSOR), shortaddr); - MqttPublishPrefixTopic_P(TELE, subtopic, Settings.flag.mqtt_sensor_retain); - } else { - MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain); - } - XdrvRulesProcess(); // apply rules } -void Z_Devices::jsonPublishNow(uint16_t shortaddr, JsonObject & values) { +void Z_Devices::jsonPublishNow(uint16_t shortaddr, Z_attribute_list &attr_list) { jsonPublishFlush(shortaddr); // flush any previous buffer - jsonAppend(shortaddr, values); + jsonAppend(shortaddr, attr_list); jsonPublishFlush(shortaddr); // publish now } @@ -1044,9 +922,8 @@ String Z_Devices::dumpLightState(uint16_t shortaddr) const { JsonObject& json = jsonBuffer.createObject(); char hex[8]; - int32_t found = findShortAddrIdx(shortaddr); - if (found >= 0) { - const Z_Device & device = devicesAt(found); + const Z_Device & device = findShortAddr(shortaddr); + if (foundDevice(device)) { const char * fname = getFriendlyName(shortaddr); bool use_fname = (Settings.flag4.zigbee_use_names) && (fname); // should we replace shortaddr with friendlyname? @@ -1090,8 +967,7 @@ String Z_Devices::dump(uint32_t dump_mode, uint16_t status_shortaddr) const { JsonArray& json = jsonBuffer.createArray(); JsonArray& devices = json; - for (std::vector::const_iterator it = _devices.begin(); it != _devices.end(); ++it) { - const Z_Device &device = **it; + for (const auto & device : _devices) { uint16_t shortaddr = device.shortaddr; char hex[22]; diff --git a/tasmota/xdrv_23_zigbee_5_converters.ino b/tasmota/xdrv_23_zigbee_5_converters.ino index 5f7c0786b..5655244d7 100644 --- a/tasmota/xdrv_23_zigbee_5_converters.ino +++ b/tasmota/xdrv_23_zigbee_5_converters.ino @@ -90,18 +90,13 @@ bool Z_isDiscreteDataType(uint8_t t) { } } -// return value: -// 0 = keep initial value -// 1 = remove initial value -typedef int32_t (*Z_AttrConverter)(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr); typedef struct Z_AttributeConverter { uint8_t type; uint8_t cluster_short; uint16_t attribute; uint16_t name_offset; int8_t multiplier; // multiplier for numerical value, (if > 0 multiply by x, if <0 device by x) - uint8_t cb; // callback func from Z_ConvOperators - // Z_AttrConverter func; + // still room for a byte } Z_AttributeConverter; // Cluster numbers are store in 8 bits format to save space, @@ -129,400 +124,391 @@ uint16_t CxToCluster(uint8_t cx) { } return 0xFFFF; } - -enum Z_ConvOperators { - Z_Nop, // copy value - Z_AddPressureUnit, // add pressure unit attribute (non numerical) - Z_ManufKeep, // copy and record Manufacturer attribute - Z_ModelKeep, // copy and record ModelId attribute - Z_AqaraSensor, // decode prioprietary Aqara Sensor message - Z_AqaraSensor2, // decode prioprietary Aqara Sensor message V2 - Z_AqaraVibration, // decode Aqara vibration modes - Z_AqaraCube, // decode Aqara cube - Z_AqaraButton, // decode Aqara button - Z_BatteryPercentage, // memorize Battery Percentage in RAM -}; - // list of post-processing directives const Z_AttributeConverter Z_PostProcess[] PROGMEM = { - { Zuint8, Cx0000, 0x0000, Z_(ZCLVersion), 1, Z_Nop }, - { Zuint8, Cx0000, 0x0001, Z_(AppVersion), 1, Z_Nop }, - { Zuint8, Cx0000, 0x0002, Z_(StackVersion), 1, Z_Nop }, - { Zuint8, Cx0000, 0x0003, Z_(HWVersion), 1, Z_Nop }, - { Zstring, Cx0000, 0x0004, Z_(Manufacturer), 1, Z_ManufKeep }, // record Manufacturer - { Zstring, Cx0000, 0x0005, Z_(ModelId), 1, Z_ModelKeep }, // record Model - { Zstring, Cx0000, 0x0006, Z_(DateCode), 1, Z_Nop }, - { Zenum8, Cx0000, 0x0007, Z_(PowerSource), 1, Z_Nop }, - { Zenum8, Cx0000, 0x0008, Z_(GenericDeviceClass), 1, Z_Nop }, - { Zenum8, Cx0000, 0x0009, Z_(GenericDeviceType), 1, Z_Nop }, - { Zoctstr, Cx0000, 0x000A, Z_(ProductCode), 1, Z_Nop }, - { Zstring, Cx0000, 0x000B, Z_(ProductURL), 1, Z_Nop }, - { Zstring, Cx0000, 0x4000, Z_(SWBuildID), 1, Z_Nop }, - // { Zunk, Cx0000, 0xFFFF, nullptr, 0, Z_Nop }, // Remove all other values + { Zuint8, Cx0000, 0x0000, Z_(ZCLVersion), 1 }, + { Zuint8, Cx0000, 0x0001, Z_(AppVersion), 1 }, + { Zuint8, Cx0000, 0x0002, Z_(StackVersion), 1 }, + { Zuint8, Cx0000, 0x0003, Z_(HWVersion), 1 }, + { Zstring, Cx0000, 0x0004, Z_(Manufacturer), 1 }, // record Manufacturer + { Zstring, Cx0000, 0x0005, Z_(ModelId), 1 }, // record Model + // { Zstring, Cx0000, 0x0004, Z_(Manufacturer), 1, Z_ManufKeep }, // record Manufacturer + // { Zstring, Cx0000, 0x0005, Z_(ModelId), 1, Z_ModelKeep }, // record Model + { Zstring, Cx0000, 0x0006, Z_(DateCode), 1 }, + { Zenum8, Cx0000, 0x0007, Z_(PowerSource), 1 }, + { Zenum8, Cx0000, 0x0008, Z_(GenericDeviceClass), 1 }, + { Zenum8, Cx0000, 0x0009, Z_(GenericDeviceType), 1 }, + { Zoctstr, Cx0000, 0x000A, Z_(ProductCode), 1 }, + { Zstring, Cx0000, 0x000B, Z_(ProductURL), 1 }, + { Zstring, Cx0000, 0x4000, Z_(SWBuildID), 1 }, + // { Zunk, Cx0000, 0xFFFF, nullptr, 0 }, // Remove all other values // Cmd 0x0A - Cluster 0x0000, attribute 0xFF01 - proprietary - { Zmap8, Cx0000, 0xFF01, Z_(), 0, Z_AqaraSensor }, - { Zmap8, Cx0000, 0xFF02, Z_(), 0, Z_AqaraSensor2 }, + { Zmap8, Cx0000, 0xFF01, Z_(), 0 }, + { Zmap8, Cx0000, 0xFF02, Z_(), 0 }, + // { Zmap8, Cx0000, 0xFF01, Z_(), 0, Z_AqaraSensor }, + // { Zmap8, Cx0000, 0xFF02, Z_(), 0, Z_AqaraSensor2 }, // Power Configuration cluster - { Zuint16, Cx0001, 0x0000, Z_(MainsVoltage), 1, Z_Nop }, - { Zuint8, Cx0001, 0x0001, Z_(MainsFrequency), 1, Z_Nop }, - { Zuint8, Cx0001, 0x0020, Z_(BatteryVoltage), -10,Z_Nop }, // divide by 10 - { Zuint8, Cx0001, 0x0021, Z_(BatteryPercentage), -2, Z_BatteryPercentage }, // divide by 2 + { Zuint16, Cx0001, 0x0000, Z_(MainsVoltage), 1 }, + { Zuint8, Cx0001, 0x0001, Z_(MainsFrequency), 1 }, + { Zuint8, Cx0001, 0x0020, Z_(BatteryVoltage), -10 }, // divide by 10 + { Zuint8, Cx0001, 0x0021, Z_(BatteryPercentage), -2 }, // divide by 2 + // { Zuint8, Cx0001, 0x0021, Z_(BatteryPercentage), -2, Z_BatteryPercentage }, // divide by 2 // Device Temperature Configuration cluster - { Zint16, Cx0002, 0x0000, Z_(CurrentTemperature), 1, Z_Nop }, - { Zint16, Cx0002, 0x0001, Z_(MinTempExperienced), 1, Z_Nop }, - { Zint16, Cx0002, 0x0002, Z_(MaxTempExperienced), 1, Z_Nop }, - { Zuint16, Cx0002, 0x0003, Z_(OverTempTotalDwell), 1, Z_Nop }, + { Zint16, Cx0002, 0x0000, Z_(CurrentTemperature), 1 }, + { Zint16, Cx0002, 0x0001, Z_(MinTempExperienced), 1 }, + { Zint16, Cx0002, 0x0002, Z_(MaxTempExperienced), 1 }, + { Zuint16, Cx0002, 0x0003, Z_(OverTempTotalDwell), 1 }, // Identify cluster - { Zuint16, Cx0003, 0x0000, Z_(IdentifyTime), 1, Z_Nop }, + { Zuint16, Cx0003, 0x0000, Z_(IdentifyTime), 1 }, // Groups cluster - { Zmap8, Cx0004, 0x0000, Z_(GroupNameSupport), 1, Z_Nop }, + { Zmap8, Cx0004, 0x0000, Z_(GroupNameSupport), 1 }, // Scenes cluster - { Zuint8, Cx0005, 0x0000, Z_(SceneCount), 1, Z_Nop }, - { Zuint8, Cx0005, 0x0001, Z_(CurrentScene), 1, Z_Nop }, - { Zuint16, Cx0005, 0x0002, Z_(CurrentGroup), 1, Z_Nop }, - { Zbool, Cx0005, 0x0003, Z_(SceneValid), 1, Z_Nop }, - //{ Zmap8, Cx0005, 0x0004, (NameSupport), 1, Z_Nop }, + { Zuint8, Cx0005, 0x0000, Z_(SceneCount), 1 }, + { Zuint8, Cx0005, 0x0001, Z_(CurrentScene), 1 }, + { Zuint16, Cx0005, 0x0002, Z_(CurrentGroup), 1 }, + { Zbool, Cx0005, 0x0003, Z_(SceneValid), 1 }, + //{ Zmap8, Cx0005, 0x0004, (NameSupport), 1 }, // On/off cluster - { Zbool, Cx0006, 0x0000, Z_(Power), 1, Z_Nop }, - { Zenum8, Cx0006, 0x4003, Z_(StartUpOnOff), 1, Z_Nop }, - { Zbool, Cx0006, 0x8000, Z_(Power), 1, Z_Nop }, // See 7280 + { Zbool, Cx0006, 0x0000, Z_(Power), 1 }, + { Zenum8, Cx0006, 0x4003, Z_(StartUpOnOff), 1 }, + { Zbool, Cx0006, 0x8000, Z_(Power), 1 }, // See 7280 // On/Off Switch Configuration cluster - { Zenum8, Cx0007, 0x0000, Z_(SwitchType), 1, Z_Nop }, + { Zenum8, Cx0007, 0x0000, Z_(SwitchType), 1 }, // Level Control cluster - { Zuint8, Cx0008, 0x0000, Z_(Dimmer), 1, Z_Nop }, - { Zmap8, Cx0008, 0x000F, Z_(DimmerOptions), 1, Z_Nop }, - { Zuint16, Cx0008, 0x0001, Z_(DimmerRemainingTime), 1, Z_Nop }, - { Zuint16, Cx0008, 0x0010, Z_(OnOffTransitionTime), 1, Z_Nop }, - // { Zuint8, Cx0008, 0x0011, (OnLevel), 1, Z_Nop }, - // { Zuint16, Cx0008, 0x0012, (OnTransitionTime), 1, Z_Nop }, - // { Zuint16, Cx0008, 0x0013, (OffTransitionTime), 1, Z_Nop }, - // { Zuint16, Cx0008, 0x0014, (DefaultMoveRate), 1, Z_Nop }, + { Zuint8, Cx0008, 0x0000, Z_(Dimmer), 1 }, + { Zmap8, Cx0008, 0x000F, Z_(DimmerOptions), 1 }, + { Zuint16, Cx0008, 0x0001, Z_(DimmerRemainingTime), 1 }, + { Zuint16, Cx0008, 0x0010, Z_(OnOffTransitionTime), 1 }, + // { Zuint8, Cx0008, 0x0011, (OnLevel), 1 }, + // { Zuint16, Cx0008, 0x0012, (OnTransitionTime), 1 }, + // { Zuint16, Cx0008, 0x0013, (OffTransitionTime), 1 }, + // { Zuint16, Cx0008, 0x0014, (DefaultMoveRate), 1 }, // Alarms cluster - { Zuint16, Cx0009, 0x0000, Z_(AlarmCount), 1, Z_Nop }, + { Zuint16, Cx0009, 0x0000, Z_(AlarmCount), 1 }, // Time cluster - { ZUTC, Cx000A, 0x0000, Z_(Time), 1, Z_Nop }, - { Zmap8, Cx000A, 0x0001, Z_(TimeStatus), 1, Z_Nop }, - { Zint32, Cx000A, 0x0002, Z_(TimeZone), 1, Z_Nop }, - { Zuint32, Cx000A, 0x0003, Z_(DstStart), 1, Z_Nop }, - { Zuint32, Cx000A, 0x0004, Z_(DstEnd), 1, Z_Nop }, - { Zint32, Cx000A, 0x0005, Z_(DstShift), 1, Z_Nop }, - { Zuint32, Cx000A, 0x0006, Z_(StandardTime), 1, Z_Nop }, - { Zuint32, Cx000A, 0x0007, Z_(LocalTime), 1, Z_Nop }, - { ZUTC, Cx000A, 0x0008, Z_(LastSetTime), 1, Z_Nop }, - { ZUTC, Cx000A, 0x0009, Z_(ValidUntilTime), 1, Z_Nop }, - { ZUTC, Cx000A, 0xFF00, Z_(TimeEpoch), 1, Z_Nop }, // Tasmota specific, epoch + { ZUTC, Cx000A, 0x0000, Z_(Time), 1 }, + { Zmap8, Cx000A, 0x0001, Z_(TimeStatus), 1 }, + { Zint32, Cx000A, 0x0002, Z_(TimeZone), 1 }, + { Zuint32, Cx000A, 0x0003, Z_(DstStart), 1 }, + { Zuint32, Cx000A, 0x0004, Z_(DstEnd), 1 }, + { Zint32, Cx000A, 0x0005, Z_(DstShift), 1 }, + { Zuint32, Cx000A, 0x0006, Z_(StandardTime), 1 }, + { Zuint32, Cx000A, 0x0007, Z_(LocalTime), 1 }, + { ZUTC, Cx000A, 0x0008, Z_(LastSetTime), 1 }, + { ZUTC, Cx000A, 0x0009, Z_(ValidUntilTime), 1 }, + { ZUTC, Cx000A, 0xFF00, Z_(TimeEpoch), 1 }, // Tasmota specific, epoch // RSSI Location cluster - { Zdata8, Cx000B, 0x0000, Z_(LocationType), 1, Z_Nop }, - { Zenum8, Cx000B, 0x0001, Z_(LocationMethod), 1, Z_Nop }, - { Zuint16, Cx000B, 0x0002, Z_(LocationAge), 1, Z_Nop }, - { Zuint8, Cx000B, 0x0003, Z_(QualityMeasure), 1, Z_Nop }, - { Zuint8, Cx000B, 0x0004, Z_(NumberOfDevices), 1, Z_Nop }, + { Zdata8, Cx000B, 0x0000, Z_(LocationType), 1 }, + { Zenum8, Cx000B, 0x0001, Z_(LocationMethod), 1 }, + { Zuint16, Cx000B, 0x0002, Z_(LocationAge), 1 }, + { Zuint8, Cx000B, 0x0003, Z_(QualityMeasure), 1 }, + { Zuint8, Cx000B, 0x0004, Z_(NumberOfDevices), 1 }, // Analog Input cluster - // { 0xFF, Cx000C, 0x0004, (AnalogInActiveText), 1, Z_Nop }, - { Zstring, Cx000C, 0x001C, Z_(AnalogInDescription), 1, Z_Nop }, - // { 0xFF, Cx000C, 0x002E, (AnalogInInactiveText), 1, Z_Nop }, - { Zsingle, Cx000C, 0x0041, Z_(AnalogInMaxValue), 1, Z_Nop }, - { Zsingle, Cx000C, 0x0045, Z_(AnalogInMinValue), 1, Z_Nop }, - { Zbool, Cx000C, 0x0051, Z_(AnalogInOutOfService), 1, Z_Nop }, - { Zsingle, Cx000C, 0x0055, Z_(AqaraRotate), 1, Z_Nop }, - // { 0xFF, Cx000C, 0x0057, (AnalogInPriorityArray),1, Z_Nop }, - { Zenum8, Cx000C, 0x0067, Z_(AnalogInReliability), 1, Z_Nop }, - // { 0xFF, Cx000C, 0x0068, (AnalogInRelinquishDefault),1, Z_Nop }, - { Zsingle, Cx000C, 0x006A, Z_(AnalogInResolution), 1, Z_Nop }, - { Zmap8, Cx000C, 0x006F, Z_(AnalogInStatusFlags), 1, Z_Nop }, - { Zenum16, Cx000C, 0x0075, Z_(AnalogInEngineeringUnits),1, Z_Nop }, - { Zuint32, Cx000C, 0x0100, Z_(AnalogInApplicationType),1, Z_Nop }, - { Zuint16, Cx000C, 0xFF05, Z_(Aqara_FF05), 1, Z_Nop }, + // { 0xFF, Cx000C, 0x0004, (AnalogInActiveText), 1 }, + { Zstring, Cx000C, 0x001C, Z_(AnalogInDescription), 1 }, + // { 0xFF, Cx000C, 0x002E, (AnalogInInactiveText), 1 }, + { Zsingle, Cx000C, 0x0041, Z_(AnalogInMaxValue), 1 }, + { Zsingle, Cx000C, 0x0045, Z_(AnalogInMinValue), 1 }, + { Zbool, Cx000C, 0x0051, Z_(AnalogInOutOfService), 1 }, + { Zsingle, Cx000C, 0x0055, Z_(AqaraRotate), 1 }, + // { 0xFF, Cx000C, 0x0057, (AnalogInPriorityArray),1 }, + { Zenum8, Cx000C, 0x0067, Z_(AnalogInReliability), 1 }, + // { 0xFF, Cx000C, 0x0068, (AnalogInRelinquishDefault),1 }, + { Zsingle, Cx000C, 0x006A, Z_(AnalogInResolution), 1 }, + { Zmap8, Cx000C, 0x006F, Z_(AnalogInStatusFlags), 1 }, + { Zenum16, Cx000C, 0x0075, Z_(AnalogInEngineeringUnits),1 }, + { Zuint32, Cx000C, 0x0100, Z_(AnalogInApplicationType),1 }, + { Zuint16, Cx000C, 0xFF05, Z_(Aqara_FF05), 1 }, // Analog Output cluster - { Zstring, Cx000D, 0x001C, Z_(AnalogOutDescription), 1, Z_Nop }, - { Zsingle, Cx000D, 0x0041, Z_(AnalogOutMaxValue), 1, Z_Nop }, - { Zsingle, Cx000D, 0x0045, Z_(AnalogOutMinValue), 1, Z_Nop }, - { Zbool, Cx000D, 0x0051, Z_(AnalogOutOutOfService),1, Z_Nop }, - { Zsingle, Cx000D, 0x0055, Z_(AnalogOutValue), 1, Z_Nop }, - // { Zunk, Cx000D, 0x0057, (AnalogOutPriorityArray),1, Z_Nop }, - { Zenum8, Cx000D, 0x0067, Z_(AnalogOutReliability), 1, Z_Nop }, - { Zsingle, Cx000D, 0x0068, Z_(AnalogOutRelinquishDefault),1, Z_Nop }, - { Zsingle, Cx000D, 0x006A, Z_(AnalogOutResolution), 1, Z_Nop }, - { Zmap8, Cx000D, 0x006F, Z_(AnalogOutStatusFlags), 1, Z_Nop }, - { Zenum16, Cx000D, 0x0075, Z_(AnalogOutEngineeringUnits),1, Z_Nop }, - { Zuint32, Cx000D, 0x0100, Z_(AnalogOutApplicationType),1, Z_Nop }, + { Zstring, Cx000D, 0x001C, Z_(AnalogOutDescription), 1 }, + { Zsingle, Cx000D, 0x0041, Z_(AnalogOutMaxValue), 1 }, + { Zsingle, Cx000D, 0x0045, Z_(AnalogOutMinValue), 1 }, + { Zbool, Cx000D, 0x0051, Z_(AnalogOutOutOfService),1 }, + { Zsingle, Cx000D, 0x0055, Z_(AnalogOutValue), 1 }, + // { Zunk, Cx000D, 0x0057, (AnalogOutPriorityArray),1 }, + { Zenum8, Cx000D, 0x0067, Z_(AnalogOutReliability), 1 }, + { Zsingle, Cx000D, 0x0068, Z_(AnalogOutRelinquishDefault),1 }, + { Zsingle, Cx000D, 0x006A, Z_(AnalogOutResolution), 1 }, + { Zmap8, Cx000D, 0x006F, Z_(AnalogOutStatusFlags), 1 }, + { Zenum16, Cx000D, 0x0075, Z_(AnalogOutEngineeringUnits),1 }, + { Zuint32, Cx000D, 0x0100, Z_(AnalogOutApplicationType),1 }, // Analog Value cluster - { Zstring, Cx000E, 0x001C, Z_(AnalogDescription), 1, Z_Nop }, - { Zbool, Cx000E, 0x0051, Z_(AnalogOutOfService), 1, Z_Nop }, - { Zsingle, Cx000E, 0x0055, Z_(AnalogValue), 1, Z_Nop }, - { Zunk, Cx000E, 0x0057, Z_(AnalogPriorityArray), 1, Z_Nop }, - { Zenum8, Cx000E, 0x0067, Z_(AnalogReliability), 1, Z_Nop }, - { Zsingle, Cx000E, 0x0068, Z_(AnalogRelinquishDefault),1, Z_Nop }, - { Zmap8, Cx000E, 0x006F, Z_(AnalogStatusFlags), 1, Z_Nop }, - { Zenum16, Cx000E, 0x0075, Z_(AnalogEngineeringUnits),1, Z_Nop }, - { Zuint32, Cx000E, 0x0100, Z_(AnalogApplicationType),1, Z_Nop }, + { Zstring, Cx000E, 0x001C, Z_(AnalogDescription), 1 }, + { Zbool, Cx000E, 0x0051, Z_(AnalogOutOfService), 1 }, + { Zsingle, Cx000E, 0x0055, Z_(AnalogValue), 1 }, + { Zunk, Cx000E, 0x0057, Z_(AnalogPriorityArray), 1 }, + { Zenum8, Cx000E, 0x0067, Z_(AnalogReliability), 1 }, + { Zsingle, Cx000E, 0x0068, Z_(AnalogRelinquishDefault),1 }, + { Zmap8, Cx000E, 0x006F, Z_(AnalogStatusFlags), 1 }, + { Zenum16, Cx000E, 0x0075, Z_(AnalogEngineeringUnits),1 }, + { Zuint32, Cx000E, 0x0100, Z_(AnalogApplicationType),1 }, // Binary Input cluster - { Zstring, Cx000F, 0x0004, Z_(BinaryInActiveText), 1, Z_Nop }, - { Zstring, Cx000F, 0x001C, Z_(BinaryInDescription), 1, Z_Nop }, - { Zstring, Cx000F, 0x002E, Z_(BinaryInInactiveText),1, Z_Nop }, - { Zbool, Cx000F, 0x0051, Z_(BinaryInOutOfService),1, Z_Nop }, - { Zenum8, Cx000F, 0x0054, Z_(BinaryInPolarity), 1, Z_Nop }, - { Zstring, Cx000F, 0x0055, Z_(BinaryInValue), 1, Z_Nop }, - // { 0xFF, Cx000F, 0x0057, (BinaryInPriorityArray),1, Z_Nop }, - { Zenum8, Cx000F, 0x0067, Z_(BinaryInReliability), 1, Z_Nop }, - { Zmap8, Cx000F, 0x006F, Z_(BinaryInStatusFlags), 1, Z_Nop }, - { Zuint32, Cx000F, 0x0100, Z_(BinaryInApplicationType),1, Z_Nop }, + { Zstring, Cx000F, 0x0004, Z_(BinaryInActiveText), 1 }, + { Zstring, Cx000F, 0x001C, Z_(BinaryInDescription), 1 }, + { Zstring, Cx000F, 0x002E, Z_(BinaryInInactiveText),1 }, + { Zbool, Cx000F, 0x0051, Z_(BinaryInOutOfService),1 }, + { Zenum8, Cx000F, 0x0054, Z_(BinaryInPolarity), 1 }, + { Zstring, Cx000F, 0x0055, Z_(BinaryInValue), 1 }, + // { 0xFF, Cx000F, 0x0057, (BinaryInPriorityArray),1 }, + { Zenum8, Cx000F, 0x0067, Z_(BinaryInReliability), 1 }, + { Zmap8, Cx000F, 0x006F, Z_(BinaryInStatusFlags), 1 }, + { Zuint32, Cx000F, 0x0100, Z_(BinaryInApplicationType),1 }, // Binary Output cluster - { Zstring, Cx0010, 0x0004, Z_(BinaryOutActiveText), 1, Z_Nop }, - { Zstring, Cx0010, 0x001C, Z_(BinaryOutDescription), 1, Z_Nop }, - { Zstring, Cx0010, 0x002E, Z_(BinaryOutInactiveText),1, Z_Nop }, - { Zuint32, Cx0010, 0x0042, Z_(BinaryOutMinimumOffTime),1, Z_Nop }, - { Zuint32, Cx0010, 0x0043, Z_(BinaryOutMinimumOnTime),1, Z_Nop }, - { Zbool, Cx0010, 0x0051, Z_(BinaryOutOutOfService),1, Z_Nop }, - { Zenum8, Cx0010, 0x0054, Z_(BinaryOutPolarity), 1, Z_Nop }, - { Zbool, Cx0010, 0x0055, Z_(BinaryOutValue), 1, Z_Nop }, - // { Zunk, Cx0010, 0x0057, (BinaryOutPriorityArray),1, Z_Nop }, - { Zenum8, Cx0010, 0x0067, Z_(BinaryOutReliability), 1, Z_Nop }, - { Zbool, Cx0010, 0x0068, Z_(BinaryOutRelinquishDefault),1, Z_Nop }, - { Zmap8, Cx0010, 0x006F, Z_(BinaryOutStatusFlags), 1, Z_Nop }, - { Zuint32, Cx0010, 0x0100, Z_(BinaryOutApplicationType),1, Z_Nop }, + { Zstring, Cx0010, 0x0004, Z_(BinaryOutActiveText), 1 }, + { Zstring, Cx0010, 0x001C, Z_(BinaryOutDescription), 1 }, + { Zstring, Cx0010, 0x002E, Z_(BinaryOutInactiveText),1 }, + { Zuint32, Cx0010, 0x0042, Z_(BinaryOutMinimumOffTime),1 }, + { Zuint32, Cx0010, 0x0043, Z_(BinaryOutMinimumOnTime),1 }, + { Zbool, Cx0010, 0x0051, Z_(BinaryOutOutOfService),1 }, + { Zenum8, Cx0010, 0x0054, Z_(BinaryOutPolarity), 1 }, + { Zbool, Cx0010, 0x0055, Z_(BinaryOutValue), 1 }, + // { Zunk, Cx0010, 0x0057, (BinaryOutPriorityArray),1 }, + { Zenum8, Cx0010, 0x0067, Z_(BinaryOutReliability), 1 }, + { Zbool, Cx0010, 0x0068, Z_(BinaryOutRelinquishDefault),1 }, + { Zmap8, Cx0010, 0x006F, Z_(BinaryOutStatusFlags), 1 }, + { Zuint32, Cx0010, 0x0100, Z_(BinaryOutApplicationType),1 }, // Binary Value cluster - { Zstring, Cx0011, 0x0004, Z_(BinaryActiveText), 1, Z_Nop }, - { Zstring, Cx0011, 0x001C, Z_(BinaryDescription), 1, Z_Nop }, - { Zstring, Cx0011, 0x002E, Z_(BinaryInactiveText), 1, Z_Nop }, - { Zuint32, Cx0011, 0x0042, Z_(BinaryMinimumOffTime), 1, Z_Nop }, - { Zuint32, Cx0011, 0x0043, Z_(BinaryMinimumOnTime), 1, Z_Nop }, - { Zbool, Cx0011, 0x0051, Z_(BinaryOutOfService), 1, Z_Nop }, - { Zbool, Cx0011, 0x0055, Z_(BinaryValue), 1, Z_Nop }, - // { Zunk, Cx0011, 0x0057, (BinaryPriorityArray), 1, Z_Nop }, - { Zenum8, Cx0011, 0x0067, Z_(BinaryReliability), 1, Z_Nop }, - { Zbool, Cx0011, 0x0068, Z_(BinaryRelinquishDefault),1, Z_Nop }, - { Zmap8, Cx0011, 0x006F, Z_(BinaryStatusFlags), 1, Z_Nop }, - { Zuint32, Cx0011, 0x0100, Z_(BinaryApplicationType),1, Z_Nop }, + { Zstring, Cx0011, 0x0004, Z_(BinaryActiveText), 1 }, + { Zstring, Cx0011, 0x001C, Z_(BinaryDescription), 1 }, + { Zstring, Cx0011, 0x002E, Z_(BinaryInactiveText), 1 }, + { Zuint32, Cx0011, 0x0042, Z_(BinaryMinimumOffTime), 1 }, + { Zuint32, Cx0011, 0x0043, Z_(BinaryMinimumOnTime), 1 }, + { Zbool, Cx0011, 0x0051, Z_(BinaryOutOfService), 1 }, + { Zbool, Cx0011, 0x0055, Z_(BinaryValue), 1 }, + // { Zunk, Cx0011, 0x0057, (BinaryPriorityArray), 1 }, + { Zenum8, Cx0011, 0x0067, Z_(BinaryReliability), 1 }, + { Zbool, Cx0011, 0x0068, Z_(BinaryRelinquishDefault),1 }, + { Zmap8, Cx0011, 0x006F, Z_(BinaryStatusFlags), 1 }, + { Zuint32, Cx0011, 0x0100, Z_(BinaryApplicationType),1 }, // Multistate Input cluster - // { Zunk, Cx0012, 0x000E, (MultiInStateText), 1, Z_Nop }, - { Zstring, Cx0012, 0x001C, Z_(MultiInDescription), 1, Z_Nop }, - { Zuint16, Cx0012, 0x004A, Z_(MultiInNumberOfStates),1, Z_Nop }, - { Zbool, Cx0012, 0x0051, Z_(MultiInOutOfService), 1, Z_Nop }, - { Zuint16, Cx0012, 0x0055, Z_(MultiInValue), 0, Z_AqaraCube }, - { Zuint16, Cx0012, 0x0055, Z_(MultiInValue), 0, Z_AqaraButton }, - { Zenum8, Cx0012, 0x0067, Z_(MultiInReliability), 1, Z_Nop }, - { Zmap8, Cx0012, 0x006F, Z_(MultiInStatusFlags), 1, Z_Nop }, - { Zuint32, Cx0012, 0x0100, Z_(MultiInApplicationType),1, Z_Nop }, + // { Zunk, Cx0012, 0x000E, (MultiInStateText), 1 }, + { Zstring, Cx0012, 0x001C, Z_(MultiInDescription), 1 }, + { Zuint16, Cx0012, 0x004A, Z_(MultiInNumberOfStates),1 }, + { Zbool, Cx0012, 0x0051, Z_(MultiInOutOfService), 1 }, + { Zuint16, Cx0012, 0x0055, Z_(MultiInValue), 1 }, + // { Zuint16, Cx0012, 0x0055, Z_(MultiInValue), 0, Z_AqaraCube }, + // { Zuint16, Cx0012, 0x0055, Z_(MultiInValue), 0, Z_AqaraButton }, + { Zenum8, Cx0012, 0x0067, Z_(MultiInReliability), 1 }, + { Zmap8, Cx0012, 0x006F, Z_(MultiInStatusFlags), 1 }, + { Zuint32, Cx0012, 0x0100, Z_(MultiInApplicationType),1 }, // Multistate output - // { Zunk, Cx0013, 0x000E, (MultiOutStateText), 1, Z_Nop }, - { Zstring, Cx0013, 0x001C, Z_(MultiOutDescription), 1, Z_Nop }, - { Zuint16, Cx0013, 0x004A, Z_(MultiOutNumberOfStates),1, Z_Nop }, - { Zbool, Cx0013, 0x0051, Z_(MultiOutOutOfService), 1, Z_Nop }, - { Zuint16, Cx0013, 0x0055, Z_(MultiOutValue), 1, Z_Nop }, - // { Zunk, Cx0013, 0x0057, (MultiOutPriorityArray),1, Z_Nop }, - { Zenum8, Cx0013, 0x0067, Z_(MultiOutReliability), 1, Z_Nop }, - { Zuint16, Cx0013, 0x0068, Z_(MultiOutRelinquishDefault),1, Z_Nop }, - { Zmap8, Cx0013, 0x006F, Z_(MultiOutStatusFlags), 1, Z_Nop }, - { Zuint32, Cx0013, 0x0100, Z_(MultiOutApplicationType),1, Z_Nop }, + // { Zunk, Cx0013, 0x000E, (MultiOutStateText), 1 }, + { Zstring, Cx0013, 0x001C, Z_(MultiOutDescription), 1 }, + { Zuint16, Cx0013, 0x004A, Z_(MultiOutNumberOfStates),1 }, + { Zbool, Cx0013, 0x0051, Z_(MultiOutOutOfService), 1 }, + { Zuint16, Cx0013, 0x0055, Z_(MultiOutValue), 1 }, + // { Zunk, Cx0013, 0x0057, (MultiOutPriorityArray),1 }, + { Zenum8, Cx0013, 0x0067, Z_(MultiOutReliability), 1 }, + { Zuint16, Cx0013, 0x0068, Z_(MultiOutRelinquishDefault),1 }, + { Zmap8, Cx0013, 0x006F, Z_(MultiOutStatusFlags), 1 }, + { Zuint32, Cx0013, 0x0100, Z_(MultiOutApplicationType),1 }, // Multistate Value cluster - // { Zunk, Cx0014, 0x000E, (MultiStateText), 1, Z_Nop }, - { Zstring, Cx0014, 0x001C, Z_(MultiDescription), 1, Z_Nop }, - { Zuint16, Cx0014, 0x004A, Z_(MultiNumberOfStates), 1, Z_Nop }, - { Zbool, Cx0014, 0x0051, Z_(MultiOutOfService), 1, Z_Nop }, - { Zuint16, Cx0014, 0x0055, Z_(MultiValue), 1, Z_Nop }, - { Zenum8, Cx0014, 0x0067, Z_(MultiReliability), 1, Z_Nop }, - { Zuint16, Cx0014, 0x0068, Z_(MultiRelinquishDefault),1, Z_Nop }, - { Zmap8, Cx0014, 0x006F, Z_(MultiStatusFlags), 1, Z_Nop }, - { Zuint32, Cx0014, 0x0100, Z_(MultiApplicationType), 1, Z_Nop }, + // { Zunk, Cx0014, 0x000E, (MultiStateText), 1 }, + { Zstring, Cx0014, 0x001C, Z_(MultiDescription), 1 }, + { Zuint16, Cx0014, 0x004A, Z_(MultiNumberOfStates), 1 }, + { Zbool, Cx0014, 0x0051, Z_(MultiOutOfService), 1 }, + { Zuint16, Cx0014, 0x0055, Z_(MultiValue), 1 }, + { Zenum8, Cx0014, 0x0067, Z_(MultiReliability), 1 }, + { Zuint16, Cx0014, 0x0068, Z_(MultiRelinquishDefault),1 }, + { Zmap8, Cx0014, 0x006F, Z_(MultiStatusFlags), 1 }, + { Zuint32, Cx0014, 0x0100, Z_(MultiApplicationType), 1 }, // Power Profile cluster - { Zuint8, Cx001A, 0x0000, Z_(TotalProfileNum), 1, Z_Nop }, - { Zbool, Cx001A, 0x0001, Z_(MultipleScheduling), 1, Z_Nop }, - { Zmap8, Cx001A, 0x0002, Z_(EnergyFormatting), 1, Z_Nop }, - { Zbool, Cx001A, 0x0003, Z_(EnergyRemote), 1, Z_Nop }, - { Zmap8, Cx001A, 0x0004, Z_(ScheduleMode), 1, Z_Nop }, + { Zuint8, Cx001A, 0x0000, Z_(TotalProfileNum), 1 }, + { Zbool, Cx001A, 0x0001, Z_(MultipleScheduling), 1 }, + { Zmap8, Cx001A, 0x0002, Z_(EnergyFormatting), 1 }, + { Zbool, Cx001A, 0x0003, Z_(EnergyRemote), 1 }, + { Zmap8, Cx001A, 0x0004, Z_(ScheduleMode), 1 }, // Poll Control cluster - { Zuint32, Cx0020, 0x0000, Z_(CheckinInterval), 1, Z_Nop }, - { Zuint32, Cx0020, 0x0001, Z_(LongPollInterval), 1, Z_Nop }, - { Zuint16, Cx0020, 0x0002, Z_(ShortPollInterval), 1, Z_Nop }, - { Zuint16, Cx0020, 0x0003, Z_(FastPollTimeout), 1, Z_Nop }, - { Zuint32, Cx0020, 0x0004, Z_(CheckinIntervalMin), 1, Z_Nop }, - { Zuint32, Cx0020, 0x0005, Z_(LongPollIntervalMin), 1, Z_Nop }, - { Zuint16, Cx0020, 0x0006, Z_(FastPollTimeoutMax), 1, Z_Nop }, + { Zuint32, Cx0020, 0x0000, Z_(CheckinInterval), 1 }, + { Zuint32, Cx0020, 0x0001, Z_(LongPollInterval), 1 }, + { Zuint16, Cx0020, 0x0002, Z_(ShortPollInterval), 1 }, + { Zuint16, Cx0020, 0x0003, Z_(FastPollTimeout), 1 }, + { Zuint32, Cx0020, 0x0004, Z_(CheckinIntervalMin), 1 }, + { Zuint32, Cx0020, 0x0005, Z_(LongPollIntervalMin), 1 }, + { Zuint16, Cx0020, 0x0006, Z_(FastPollTimeoutMax), 1 }, // Shade Configuration cluster - { Zuint16, Cx0100, 0x0000, Z_(PhysicalClosedLimit), 1, Z_Nop }, - { Zuint8, Cx0100, 0x0001, Z_(MotorStepSize), 1, Z_Nop }, - { Zmap8, Cx0100, 0x0002, Z_(Status), 1, Z_Nop }, - { Zuint16, Cx0100, 0x0010, Z_(ClosedLimit), 1, Z_Nop }, - { Zenum8, Cx0100, 0x0011, Z_(Mode), 1, Z_Nop }, + { Zuint16, Cx0100, 0x0000, Z_(PhysicalClosedLimit), 1 }, + { Zuint8, Cx0100, 0x0001, Z_(MotorStepSize), 1 }, + { Zmap8, Cx0100, 0x0002, Z_(Status), 1 }, + { Zuint16, Cx0100, 0x0010, Z_(ClosedLimit), 1 }, + { Zenum8, Cx0100, 0x0011, Z_(Mode), 1 }, // Door Lock cluster - { Zenum8, Cx0101, 0x0000, Z_(LockState), 1, Z_Nop }, - { Zenum8, Cx0101, 0x0001, Z_(LockType), 1, Z_Nop }, - { Zbool, Cx0101, 0x0002, Z_(ActuatorEnabled), 1, Z_Nop }, - { Zenum8, Cx0101, 0x0003, Z_(DoorState), 1, Z_Nop }, - { Zuint32, Cx0101, 0x0004, Z_(DoorOpenEvents), 1, Z_Nop }, - { Zuint32, Cx0101, 0x0005, Z_(DoorClosedEvents), 1, Z_Nop }, - { Zuint16, Cx0101, 0x0006, Z_(OpenPeriod), 1, Z_Nop }, + { Zenum8, Cx0101, 0x0000, Z_(LockState), 1 }, + { Zenum8, Cx0101, 0x0001, Z_(LockType), 1 }, + { Zbool, Cx0101, 0x0002, Z_(ActuatorEnabled), 1 }, + { Zenum8, Cx0101, 0x0003, Z_(DoorState), 1 }, + { Zuint32, Cx0101, 0x0004, Z_(DoorOpenEvents), 1 }, + { Zuint32, Cx0101, 0x0005, Z_(DoorClosedEvents), 1 }, + { Zuint16, Cx0101, 0x0006, Z_(OpenPeriod), 1 }, // Aqara Lumi Vibration Sensor - { Zuint16, Cx0101, 0x0055, Z_(AqaraVibrationMode), 0, Z_AqaraVibration }, - { Zuint16, Cx0101, 0x0503, Z_(AqaraVibrationsOrAngle), 1, Z_Nop }, - { Zuint32, Cx0101, 0x0505, Z_(AqaraVibration505), 1, Z_Nop }, - { Zuint48, Cx0101, 0x0508, Z_(AqaraAccelerometer), 0, Z_AqaraVibration }, + { Zuint16, Cx0101, 0x0055, Z_(AqaraVibrationMode), 1 }, + { Zuint16, Cx0101, 0x0503, Z_(AqaraVibrationsOrAngle), 1 }, + { Zuint32, Cx0101, 0x0505, Z_(AqaraVibration505), 1 }, + { Zuint48, Cx0101, 0x0508, Z_(AqaraAccelerometer), 1 }, // Window Covering cluster - { Zenum8, Cx0102, 0x0000, Z_(WindowCoveringType), 1, Z_Nop }, - { Zuint16, Cx0102, 0x0001, Z_(PhysicalClosedLimitLift),1, Z_Nop }, - { Zuint16, Cx0102, 0x0002, Z_(PhysicalClosedLimitTilt),1, Z_Nop }, - { Zuint16, Cx0102, 0x0003, Z_(CurrentPositionLift), 1, Z_Nop }, - { Zuint16, Cx0102, 0x0004, Z_(CurrentPositionTilt), 1, Z_Nop }, - { Zuint16, Cx0102, 0x0005, Z_(NumberofActuationsLift),1, Z_Nop }, - { Zuint16, Cx0102, 0x0006, Z_(NumberofActuationsTilt),1, Z_Nop }, - { Zmap8, Cx0102, 0x0007, Z_(ConfigStatus), 1, Z_Nop }, - { Zuint8, Cx0102, 0x0008, Z_(CurrentPositionLiftPercentage),1, Z_Nop }, - { Zuint8, Cx0102, 0x0009, Z_(CurrentPositionTiltPercentage),1, Z_Nop }, - { Zuint16, Cx0102, 0x0010, Z_(InstalledOpenLimitLift),1, Z_Nop }, - { Zuint16, Cx0102, 0x0011, Z_(InstalledClosedLimitLift),1, Z_Nop }, - { Zuint16, Cx0102, 0x0012, Z_(InstalledOpenLimitTilt),1, Z_Nop }, - { Zuint16, Cx0102, 0x0013, Z_(InstalledClosedLimitTilt),1, Z_Nop }, - { Zuint16, Cx0102, 0x0014, Z_(VelocityLift), 1, Z_Nop }, - { Zuint16, Cx0102, 0x0015, Z_(AccelerationTimeLift),1, Z_Nop }, - { Zuint16, Cx0102, 0x0016, Z_(DecelerationTimeLift), 1, Z_Nop }, - { Zmap8, Cx0102, 0x0017, Z_(Mode), 1, Z_Nop }, - { Zoctstr, Cx0102, 0x0018, Z_(IntermediateSetpointsLift),1, Z_Nop }, - { Zoctstr, Cx0102, 0x0019, Z_(IntermediateSetpointsTilt),1, Z_Nop }, + { Zenum8, Cx0102, 0x0000, Z_(WindowCoveringType), 1 }, + { Zuint16, Cx0102, 0x0001, Z_(PhysicalClosedLimitLift),1 }, + { Zuint16, Cx0102, 0x0002, Z_(PhysicalClosedLimitTilt),1 }, + { Zuint16, Cx0102, 0x0003, Z_(CurrentPositionLift), 1 }, + { Zuint16, Cx0102, 0x0004, Z_(CurrentPositionTilt), 1 }, + { Zuint16, Cx0102, 0x0005, Z_(NumberofActuationsLift),1 }, + { Zuint16, Cx0102, 0x0006, Z_(NumberofActuationsTilt),1 }, + { Zmap8, Cx0102, 0x0007, Z_(ConfigStatus), 1 }, + { Zuint8, Cx0102, 0x0008, Z_(CurrentPositionLiftPercentage),1 }, + { Zuint8, Cx0102, 0x0009, Z_(CurrentPositionTiltPercentage),1 }, + { Zuint16, Cx0102, 0x0010, Z_(InstalledOpenLimitLift),1 }, + { Zuint16, Cx0102, 0x0011, Z_(InstalledClosedLimitLift),1 }, + { Zuint16, Cx0102, 0x0012, Z_(InstalledOpenLimitTilt),1 }, + { Zuint16, Cx0102, 0x0013, Z_(InstalledClosedLimitTilt),1 }, + { Zuint16, Cx0102, 0x0014, Z_(VelocityLift), 1 }, + { Zuint16, Cx0102, 0x0015, Z_(AccelerationTimeLift),1 }, + { Zuint16, Cx0102, 0x0016, Z_(DecelerationTimeLift), 1 }, + { Zmap8, Cx0102, 0x0017, Z_(Mode), 1 }, + { Zoctstr, Cx0102, 0x0018, Z_(IntermediateSetpointsLift),1 }, + { Zoctstr, Cx0102, 0x0019, Z_(IntermediateSetpointsTilt),1 }, // Color Control cluster - { Zuint8, Cx0300, 0x0000, Z_(Hue), 1, Z_Nop }, - { Zuint8, Cx0300, 0x0001, Z_(Sat), 1, Z_Nop }, - { Zuint16, Cx0300, 0x0002, Z_(RemainingTime), 1, Z_Nop }, - { Zuint16, Cx0300, 0x0003, Z_(X), 1, Z_Nop }, - { Zuint16, Cx0300, 0x0004, Z_(Y), 1, Z_Nop }, - { Zenum8, Cx0300, 0x0005, Z_(DriftCompensation), 1, Z_Nop }, - { Zstring, Cx0300, 0x0006, Z_(CompensationText), 1, Z_Nop }, - { Zuint16, Cx0300, 0x0007, Z_(CT), 1, Z_Nop }, - { Zenum8, Cx0300, 0x0008, Z_(ColorMode), 1, Z_Nop }, - { Zuint8, Cx0300, 0x0010, Z_(NumberOfPrimaries), 1, Z_Nop }, - { Zuint16, Cx0300, 0x0011, Z_(Primary1X), 1, Z_Nop }, - { Zuint16, Cx0300, 0x0012, Z_(Primary1Y), 1, Z_Nop }, - { Zuint8, Cx0300, 0x0013, Z_(Primary1Intensity), 1, Z_Nop }, - { Zuint16, Cx0300, 0x0015, Z_(Primary2X), 1, Z_Nop }, - { Zuint16, Cx0300, 0x0016, Z_(Primary2Y), 1, Z_Nop }, - { Zuint8, Cx0300, 0x0017, Z_(Primary2Intensity), 1, Z_Nop }, - { Zuint16, Cx0300, 0x0019, Z_(Primary3X), 1, Z_Nop }, - { Zuint16, Cx0300, 0x001A, Z_(Primary3Y), 1, Z_Nop }, - { Zuint8, Cx0300, 0x001B, Z_(Primary3Intensity), 1, Z_Nop }, - { Zuint16, Cx0300, 0x0030, Z_(WhitePointX), 1, Z_Nop }, - { Zuint16, Cx0300, 0x0031, Z_(WhitePointY), 1, Z_Nop }, - { Zuint16, Cx0300, 0x0032, Z_(ColorPointRX), 1, Z_Nop }, - { Zuint16, Cx0300, 0x0033, Z_(ColorPointRY), 1, Z_Nop }, - { Zuint8, Cx0300, 0x0034, Z_(ColorPointRIntensity), 1, Z_Nop }, - { Zuint16, Cx0300, 0x0036, Z_(ColorPointGX), 1, Z_Nop }, - { Zuint16, Cx0300, 0x0037, Z_(ColorPointGY), 1, Z_Nop }, - { Zuint8, Cx0300, 0x0038, Z_(ColorPointGIntensity), 1, Z_Nop }, - { Zuint16, Cx0300, 0x003A, Z_(ColorPointBX), 1, Z_Nop }, - { Zuint16, Cx0300, 0x003B, Z_(ColorPointBY), 1, Z_Nop }, - { Zuint8, Cx0300, 0x003C, Z_(ColorPointBIntensity), 1, Z_Nop }, + { Zuint8, Cx0300, 0x0000, Z_(Hue), 1 }, + { Zuint8, Cx0300, 0x0001, Z_(Sat), 1 }, + { Zuint16, Cx0300, 0x0002, Z_(RemainingTime), 1 }, + { Zuint16, Cx0300, 0x0003, Z_(X), 1 }, + { Zuint16, Cx0300, 0x0004, Z_(Y), 1 }, + { Zenum8, Cx0300, 0x0005, Z_(DriftCompensation), 1 }, + { Zstring, Cx0300, 0x0006, Z_(CompensationText), 1 }, + { Zuint16, Cx0300, 0x0007, Z_(CT), 1 }, + { Zenum8, Cx0300, 0x0008, Z_(ColorMode), 1 }, + { Zuint8, Cx0300, 0x0010, Z_(NumberOfPrimaries), 1 }, + { Zuint16, Cx0300, 0x0011, Z_(Primary1X), 1 }, + { Zuint16, Cx0300, 0x0012, Z_(Primary1Y), 1 }, + { Zuint8, Cx0300, 0x0013, Z_(Primary1Intensity), 1 }, + { Zuint16, Cx0300, 0x0015, Z_(Primary2X), 1 }, + { Zuint16, Cx0300, 0x0016, Z_(Primary2Y), 1 }, + { Zuint8, Cx0300, 0x0017, Z_(Primary2Intensity), 1 }, + { Zuint16, Cx0300, 0x0019, Z_(Primary3X), 1 }, + { Zuint16, Cx0300, 0x001A, Z_(Primary3Y), 1 }, + { Zuint8, Cx0300, 0x001B, Z_(Primary3Intensity), 1 }, + { Zuint16, Cx0300, 0x0030, Z_(WhitePointX), 1 }, + { Zuint16, Cx0300, 0x0031, Z_(WhitePointY), 1 }, + { Zuint16, Cx0300, 0x0032, Z_(ColorPointRX), 1 }, + { Zuint16, Cx0300, 0x0033, Z_(ColorPointRY), 1 }, + { Zuint8, Cx0300, 0x0034, Z_(ColorPointRIntensity), 1 }, + { Zuint16, Cx0300, 0x0036, Z_(ColorPointGX), 1 }, + { Zuint16, Cx0300, 0x0037, Z_(ColorPointGY), 1 }, + { Zuint8, Cx0300, 0x0038, Z_(ColorPointGIntensity), 1 }, + { Zuint16, Cx0300, 0x003A, Z_(ColorPointBX), 1 }, + { Zuint16, Cx0300, 0x003B, Z_(ColorPointBY), 1 }, + { Zuint8, Cx0300, 0x003C, Z_(ColorPointBIntensity), 1 }, // Illuminance Measurement cluster - { Zuint16, Cx0400, 0x0000, Z_(Illuminance), 1, Z_Nop }, // Illuminance (in Lux) - { Zuint16, Cx0400, 0x0001, Z_(IlluminanceMinMeasuredValue), 1, Z_Nop }, // - { Zuint16, Cx0400, 0x0002, Z_(IlluminanceMaxMeasuredValue), 1, Z_Nop }, // - { Zuint16, Cx0400, 0x0003, Z_(IlluminanceTolerance), 1, Z_Nop }, // - { Zenum8, Cx0400, 0x0004, Z_(IlluminanceLightSensorType), 1, Z_Nop }, // - { Zunk, Cx0400, 0xFFFF, Z_(), 0, Z_Nop }, // Remove all other values + { Zuint16, Cx0400, 0x0000, Z_(Illuminance), 1 }, // Illuminance (in Lux) + { Zuint16, Cx0400, 0x0001, Z_(IlluminanceMinMeasuredValue), 1 }, // + { Zuint16, Cx0400, 0x0002, Z_(IlluminanceMaxMeasuredValue), 1 }, // + { Zuint16, Cx0400, 0x0003, Z_(IlluminanceTolerance), 1 }, // + { Zenum8, Cx0400, 0x0004, Z_(IlluminanceLightSensorType), 1 }, // + { Zunk, Cx0400, 0xFFFF, Z_(), 0 }, // Remove all other values // Illuminance Level Sensing cluster - { Zenum8, Cx0401, 0x0000, Z_(IlluminanceLevelStatus), 1, Z_Nop }, // Illuminance (in Lux) - { Zenum8, Cx0401, 0x0001, Z_(IlluminanceLightSensorType), 1, Z_Nop }, // LightSensorType - { Zuint16, Cx0401, 0x0010, Z_(IlluminanceTargetLevel), 1, Z_Nop }, // - { Zunk, Cx0401, 0xFFFF, Z_(), 0, Z_Nop }, // Remove all other values + { Zenum8, Cx0401, 0x0000, Z_(IlluminanceLevelStatus), 1 }, // Illuminance (in Lux) + { Zenum8, Cx0401, 0x0001, Z_(IlluminanceLightSensorType), 1 }, // LightSensorType + { Zuint16, Cx0401, 0x0010, Z_(IlluminanceTargetLevel), 1 }, // + { Zunk, Cx0401, 0xFFFF, Z_(), 0 }, // Remove all other values // Temperature Measurement cluster - { Zint16, Cx0402, 0x0000, Z_(Temperature), -100, Z_Nop }, // divide by 100 - { Zint16, Cx0402, 0x0001, Z_(TemperatureMinMeasuredValue), -100, Z_Nop }, // - { Zint16, Cx0402, 0x0002, Z_(TemperatureMaxMeasuredValue), -100, Z_Nop }, // - { Zuint16, Cx0402, 0x0003, Z_(TemperatureTolerance), -100, Z_Nop }, // - { Zunk, Cx0402, 0xFFFF, Z_(), 0, Z_Nop }, // Remove all other values + { Zint16, Cx0402, 0x0000, Z_(Temperature), -100 }, // divide by 100 + { Zint16, Cx0402, 0x0001, Z_(TemperatureMinMeasuredValue), -100 }, // + { Zint16, Cx0402, 0x0002, Z_(TemperatureMaxMeasuredValue), -100 }, // + { Zuint16, Cx0402, 0x0003, Z_(TemperatureTolerance), -100 }, // + { Zunk, Cx0402, 0xFFFF, Z_(), 0 }, // Remove all other values // Pressure Measurement cluster - { Zunk, Cx0403, 0x0000, Z_(PressureUnit), 0, Z_AddPressureUnit }, // Pressure Unit - { Zint16, Cx0403, 0x0000, Z_(Pressure), 1, Z_Nop }, // Pressure - { Zint16, Cx0403, 0x0001, Z_(PressureMinMeasuredValue), 1, Z_Nop }, // - { Zint16, Cx0403, 0x0002, Z_(PressureMaxMeasuredValue), 1, Z_Nop }, // - { Zuint16, Cx0403, 0x0003, Z_(PressureTolerance), 1, Z_Nop }, // - { Zint16, Cx0403, 0x0010, Z_(PressureScaledValue), 1, Z_Nop }, // - { Zint16, Cx0403, 0x0011, Z_(PressureMinScaledValue), 1, Z_Nop }, // - { Zint16, Cx0403, 0x0012, Z_(PressureMaxScaledValue), 1, Z_Nop }, // - { Zuint16, Cx0403, 0x0013, Z_(PressureScaledTolerance), 1, Z_Nop }, // - { Zint8, Cx0403, 0x0014, Z_(PressureScale), 1, Z_Nop }, // - { Zunk, Cx0403, 0xFFFF, Z_(), 0, Z_Nop }, // Remove all other Pressure values + { Zint16, Cx0403, 0x0000, Z_(Pressure), 1 }, // Pressure + { Zint16, Cx0403, 0x0001, Z_(PressureMinMeasuredValue), 1 }, // + { Zint16, Cx0403, 0x0002, Z_(PressureMaxMeasuredValue), 1 }, // + { Zuint16, Cx0403, 0x0003, Z_(PressureTolerance), 1 }, // + { Zint16, Cx0403, 0x0010, Z_(PressureScaledValue), 1 }, // + { Zint16, Cx0403, 0x0011, Z_(PressureMinScaledValue), 1 }, // + { Zint16, Cx0403, 0x0012, Z_(PressureMaxScaledValue), 1 }, // + { Zuint16, Cx0403, 0x0013, Z_(PressureScaledTolerance), 1 }, // + { Zint8, Cx0403, 0x0014, Z_(PressureScale), 1 }, // + { Zunk, Cx0403, 0xFFFF, Z_(), 0 }, // Remove all other Pressure values // Flow Measurement cluster - { Zuint16, Cx0404, 0x0000, Z_(FlowRate), -10, Z_Nop }, // Flow (in m3/h) - { Zuint16, Cx0404, 0x0001, Z_(FlowMinMeasuredValue), 1, Z_Nop }, // - { Zuint16, Cx0404, 0x0002, Z_(FlowMaxMeasuredValue), 1, Z_Nop }, // - { Zuint16, Cx0404, 0x0003, Z_(FlowTolerance), 1, Z_Nop }, // - { Zunk, Cx0404, 0xFFFF, Z_(), 0, Z_Nop }, // Remove all other values + { Zuint16, Cx0404, 0x0000, Z_(FlowRate), -10 }, // Flow (in m3/h) + { Zuint16, Cx0404, 0x0001, Z_(FlowMinMeasuredValue), 1 }, // + { Zuint16, Cx0404, 0x0002, Z_(FlowMaxMeasuredValue), 1 }, // + { Zuint16, Cx0404, 0x0003, Z_(FlowTolerance), 1 }, // + { Zunk, Cx0404, 0xFFFF, Z_(), 0 }, // Remove all other values // Relative Humidity Measurement cluster - { Zuint16, Cx0405, 0x0000, Z_(Humidity), -100, Z_Nop }, // Humidity - { Zuint16, Cx0405, 0x0001, Z_(HumidityMinMeasuredValue), 1, Z_Nop }, // - { Zuint16, Cx0405, 0x0002, Z_(HumidityMaxMeasuredValue), 1, Z_Nop }, // - { Zuint16, Cx0405, 0x0003, Z_(HumidityTolerance), 1, Z_Nop }, // - { Zunk, Cx0405, 0xFFFF, Z_(), 0, Z_Nop }, // Remove all other values + { Zuint16, Cx0405, 0x0000, Z_(Humidity), -100 }, // Humidity + { Zuint16, Cx0405, 0x0001, Z_(HumidityMinMeasuredValue), 1 }, // + { Zuint16, Cx0405, 0x0002, Z_(HumidityMaxMeasuredValue), 1 }, // + { Zuint16, Cx0405, 0x0003, Z_(HumidityTolerance), 1 }, // + { Zunk, Cx0405, 0xFFFF, Z_(), 0 }, // Remove all other values // Occupancy Sensing cluster - { Zmap8, Cx0406, 0x0000, Z_(Occupancy), 1, Z_Nop }, // Occupancy (map8) - { Zenum8, Cx0406, 0x0001, Z_(OccupancySensorType), 1, Z_Nop }, // OccupancySensorType - { Zunk, Cx0406, 0xFFFF, Z_(), 0, Z_Nop }, // Remove all other values + { Zmap8, Cx0406, 0x0000, Z_(Occupancy), 1 }, // Occupancy (map8) + { Zenum8, Cx0406, 0x0001, Z_(OccupancySensorType), 1 }, // OccupancySensorType + { Zunk, Cx0406, 0xFFFF, Z_(), 0 }, // Remove all other values // IAS Cluster (Intruder Alarm System) - { Zenum8, Cx0500, 0x0000, Z_(ZoneState), 1, Z_Nop }, // Occupancy (map8) - { Zenum16, Cx0500, 0x0001, Z_(ZoneType), 1, Z_Nop }, // Occupancy (map8) - { Zmap16, Cx0500, 0x0002, Z_(ZoneStatus), 1, Z_Nop }, // Occupancy (map8) + { Zenum8, Cx0500, 0x0000, Z_(ZoneState), 1 }, // Occupancy (map8) + { Zenum16, Cx0500, 0x0001, Z_(ZoneType), 1 }, // Occupancy (map8) + { Zmap16, Cx0500, 0x0002, Z_(ZoneStatus), 1 }, // Occupancy (map8) // Metering (Smart Energy) cluster - { Zuint48, Cx0702, 0x0000, Z_(CurrentSummDelivered), 1, Z_Nop }, + { Zuint48, Cx0702, 0x0000, Z_(CurrentSummDelivered), 1 }, // Meter Identification cluster - { Zstring, Cx0B01, 0x0000, Z_(CompanyName), 1, Z_Nop }, - { Zuint16, Cx0B01, 0x0001, Z_(MeterTypeID), 1, Z_Nop }, - { Zuint16, Cx0B01, 0x0004, Z_(DataQualityID), 1, Z_Nop }, - { Zstring, Cx0B01, 0x0005, Z_(CustomerName), 1, Z_Nop }, - { Zoctstr, Cx0B01, 0x0006, Z_(Model), 1, Z_Nop }, - { Zoctstr, Cx0B01, 0x0007, Z_(PartNumber), 1, Z_Nop }, - { Zoctstr, Cx0B01, 0x0008, Z_(ProductRevision), 1, Z_Nop }, - { Zoctstr, Cx0B01, 0x000A, Z_(SoftwareRevision), 1, Z_Nop }, - { Zstring, Cx0B01, 0x000B, Z_(UtilityName), 1, Z_Nop }, - { Zstring, Cx0B01, 0x000C, Z_(POD), 1, Z_Nop }, - { Zint24, Cx0B01, 0x000D, Z_(AvailablePower), 1, Z_Nop }, - { Zint24, Cx0B01, 0x000E, Z_(PowerThreshold), 1, Z_Nop }, + { Zstring, Cx0B01, 0x0000, Z_(CompanyName), 1 }, + { Zuint16, Cx0B01, 0x0001, Z_(MeterTypeID), 1 }, + { Zuint16, Cx0B01, 0x0004, Z_(DataQualityID), 1 }, + { Zstring, Cx0B01, 0x0005, Z_(CustomerName), 1 }, + { Zoctstr, Cx0B01, 0x0006, Z_(Model), 1 }, + { Zoctstr, Cx0B01, 0x0007, Z_(PartNumber), 1 }, + { Zoctstr, Cx0B01, 0x0008, Z_(ProductRevision), 1 }, + { Zoctstr, Cx0B01, 0x000A, Z_(SoftwareRevision), 1 }, + { Zstring, Cx0B01, 0x000B, Z_(UtilityName), 1 }, + { Zstring, Cx0B01, 0x000C, Z_(POD), 1 }, + { Zint24, Cx0B01, 0x000D, Z_(AvailablePower), 1 }, + { Zint24, Cx0B01, 0x000E, Z_(PowerThreshold), 1 }, // Electrical Measurement cluster - { Zuint16, Cx0B04, 0x0505, Z_(RMSVoltage), 1, Z_Nop }, - { Zuint16, Cx0B04, 0x0508, Z_(RMSCurrent), 1, Z_Nop }, - { Zint16, Cx0B04, 0x050B, Z_(ActivePower), 1, Z_Nop }, + { Zuint16, Cx0B04, 0x0505, Z_(RMSVoltage), 1 }, + { Zuint16, Cx0B04, 0x0508, Z_(RMSCurrent), 1 }, + { Zint16, Cx0B04, 0x050B, Z_(ActivePower), 1 }, // Diagnostics cluster - { Zuint16, Cx0B05, 0x0000, Z_(NumberOfResets), 1, Z_Nop }, - { Zuint16, Cx0B05, 0x0001, Z_(PersistentMemoryWrites),1, Z_Nop }, - { Zuint8, Cx0B05, 0x011C, Z_(LastMessageLQI), 1, Z_Nop }, - { Zuint8, Cx0B05, 0x011D, Z_(LastMessageRSSI), 1, Z_Nop }, + { Zuint16, Cx0B05, 0x0000, Z_(NumberOfResets), 1 }, + { Zuint16, Cx0B05, 0x0001, Z_(PersistentMemoryWrites),1 }, + { Zuint8, Cx0B05, 0x011C, Z_(LastMessageLQI), 1 }, + { Zuint8, Cx0B05, 0x011D, Z_(LastMessageRSSI), 1 }, }; @@ -544,8 +530,7 @@ typedef union ZCLHeaderFrameControl_t { // If not found: // - returns nullptr const __FlashStringHelper* zigbeeFindAttributeByName(const char *command, - uint16_t *cluster, uint16_t *attribute, int8_t *multiplier, - uint8_t *cb) { + uint16_t *cluster, uint16_t *attribute, int8_t *multiplier) { for (uint32_t i = 0; i < ARRAY_SIZE(Z_PostProcess); i++) { const Z_AttributeConverter *converter = &Z_PostProcess[i]; if (0 == pgm_read_word(&converter->name_offset)) { continue; } // avoid strcasecmp_P() from crashing @@ -553,7 +538,6 @@ const __FlashStringHelper* zigbeeFindAttributeByName(const char *command, if (cluster) { *cluster = CxToCluster(pgm_read_byte(&converter->cluster_short)); } if (attribute) { *attribute = pgm_read_word(&converter->attribute); } if (multiplier) { *multiplier = pgm_read_byte(&converter->multiplier); } - if (cb) { *cb = pgm_read_byte(&converter->cb); } return (const __FlashStringHelper*) (Z_strings + pgm_read_word(&converter->name_offset)); } } @@ -627,16 +611,24 @@ public: return _frame_control.b.frame_type & 1; } - static void generateAttributeName(const JsonObject& json, uint16_t cluster, uint16_t attr, char *key, size_t key_len); - void parseReportAttributes(JsonObject& json, uint8_t offset = 0); - void parseReadAttributes(JsonObject& json, uint8_t offset = 0); - void parseReadAttributesResponse(JsonObject& json, uint8_t offset = 0); - void parseReadConfigAttributes(JsonObject& json, uint8_t offset = 0); - void parseConfigAttributes(JsonObject& json, uint8_t offset = 0); + void parseReportAttributes(Z_attribute_list& attr_list); + void generateSyntheticAttributes(Z_attribute_list& attr_list); + void generateCallBacks(Z_attribute_list& attr_list); + void parseReadAttributes(Z_attribute_list& attr_list); + void parseReadAttributesResponse(Z_attribute_list& attr_list); + void parseReadConfigAttributes(Z_attribute_list& attr_list); + void parseConfigAttributes(Z_attribute_list& attr_list); void parseResponse(void); - void parseClusterSpecificCommand(JsonObject& json, uint8_t offset = 0); - void postProcessAttributes(uint16_t shortaddr, JsonObject& json); - void updateInternalAttributes(uint16_t shortaddr, JsonObject& json); + void parseResponseOld(void); + void parseClusterSpecificCommand(Z_attribute_list& attr_list); + void postProcessAttributes(uint16_t shortaddr, Z_attribute_list& attr_list); + + // synthetic attributes converters + void syntheticAqaraSensor(Z_attribute_list &attr_list, class Z_attribute &attr); + void syntheticAqaraSensor2(Z_attribute_list &attr_list, class Z_attribute &attr); + void syntheticAqaraCubeOrButton(Z_attribute_list &attr_list, class Z_attribute &attr); + void syntheticAqaraVibration(Z_attribute_list &attr_list, class Z_attribute &attr); + inline void setGroupId(uint16_t groupid) { _groupaddr = groupid; @@ -782,14 +774,13 @@ int32_t encodeSingleAttribute(class SBuffer &buf, double val_d, const char *val_ // parse a single attribute // // Input: -// json: json Object where to add the attribute -// attrid_str: the key for the attribute +// attr: attribute object to store to // buf: the buffer to read from // offset: location in the buffer to read from // attrtype: type of attribute (byte) or -1 to read from the stream as first byte // Output: // return: the length in bytes of the attribute -uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer &buf, +uint32_t parseSingleAttribute(Z_attribute & attr, const SBuffer &buf, uint32_t offset, int32_t attrtype = -1) { uint32_t i = offset; @@ -798,7 +789,7 @@ uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer } // fallback - enter a null value - json[attrid_str] = (char*) nullptr; + attr.setNone(); // set to null by default uint32_t len = Z_getDatatypeLen(attrtype); // pre-compute lenght, overloaded for variable length attributes @@ -814,7 +805,7 @@ uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer uint8_t uint8_val = buf.get8(i); // i += 1; if (0xFF != uint8_val) { - json[attrid_str] = uint8_val; + attr.setUInt(uint8_val); } } break; @@ -824,7 +815,7 @@ uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer uint16_t uint16_val = buf.get16(i); // i += 2; if (0xFFFF != uint16_val) { - json[attrid_str] = uint16_val; + attr.setUInt(uint16_val); } } break; @@ -834,7 +825,7 @@ uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer uint32_t uint32_val = buf.get32(i); // i += 4; if (0xFFFFFFFF != uint32_val) { - json[attrid_str] = uint32_val; + attr.setUInt(uint32_val); } } break; @@ -856,7 +847,7 @@ uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer for (uint32_t j=0; j= i + 3) { uint16_t attrid = _payload.get16(i); i += 2; - char key[16]; - generateAttributeName(json, _cluster_id, attrid, key, sizeof(key)); - // exception for Xiaomi lumi.weather - specific field to be treated as octet and not char if ((0x0000 == _cluster_id) && (0xFF01 == attrid)) { if (0x42 == _payload.get8(i)) { _payload.set8(i, 0x41); // change type from 0x42 to 0x41 } } - i += parseSingleAttribute(json, key, _payload, i); + + // TODO look for suffix + Z_attribute & attr = attr_list.addAttribute(_cluster_id, attrid); + + i += parseSingleAttribute(attr, _payload, i); } // Issue Philips outdoor motion sensor SML002, see https://github.com/Koenkk/zigbee2mqtt/issues/897 @@ -1061,19 +1032,67 @@ void ZCLFrame::parseReportAttributes(JsonObject& json, uint8_t offset) { } } +void ZCLFrame::generateSyntheticAttributes(Z_attribute_list& attr_list) { + // scan through attributes and apply specific converters + for (auto &attr : attr_list) { + if (attr.key_is_str) { continue; } // pass if key is a name + uint32_t ccccaaaa = (attr.key.id.cluster << 16) | attr.key.id.attr_id; + + switch (ccccaaaa) { // 0xccccaaaa . c=cluster, a=attribute + case 0x0000FF01: + syntheticAqaraSensor(attr_list, attr); + break; + case 0x0000FF02: + syntheticAqaraSensor2(attr_list, attr); + break; + case 0x00120055: + syntheticAqaraCubeOrButton(attr_list, attr); + break; + case 0x01010055: + case 0x01010508: + syntheticAqaraVibration(attr_list, attr); + break; + } + } +} + +// Set deferred callbacks for Occupancy +// TODO make delay a parameter +void ZCLFrame::generateCallBacks(Z_attribute_list& attr_list) { + static const uint32_t OCCUPANCY_TIMEOUT = 90 * 1000; // 90 s + // scan through attributes and apply specific converters + for (auto &attr : attr_list) { + if (attr.key_is_str) { continue; } // pass if key is a name + uint32_t ccccaaaa = (attr.key.id.cluster << 16) | attr.key.id.attr_id; + + switch (ccccaaaa) { // 0xccccaaaa . c=cluster, a=attribute + case 0x04060000: // Occupancy + uint32_t occupancy = attr.getUInt(); + if (occupancy) { + zigbee_devices.setTimer(_srcaddr, 0 /* groupaddr */, OCCUPANCY_TIMEOUT, _cluster_id, _srcendpoint, Z_CAT_VIRTUAL_OCCUPANCY, 0, &Z_OccupancyCallback); + } else { + zigbee_devices.resetTimersForDevice(_srcaddr, 0 /* groupaddr */, Z_CAT_VIRTUAL_OCCUPANCY); + } + break; + } + } +} + // ZCL_READ_ATTRIBUTES -// TODO -void ZCLFrame::parseReadAttributes(JsonObject& json, uint8_t offset) { - uint32_t i = offset; +void ZCLFrame::parseReadAttributes(Z_attribute_list& attr_list) { + uint32_t i = 0; uint32_t len = _payload.len(); - json[F(D_CMND_ZIGBEE_CLUSTER)] = _cluster_id; + uint16_t read_attr_ids[len/2]; - JsonArray &attr_list = json.createNestedArray(F("Read")); - JsonObject &attr_names = json.createNestedObject(F("ReadNames")); + attr_list.addAttribute(F(D_CMND_ZIGBEE_CLUSTER)).setUInt(_cluster_id); + + Z_json_array attr_numbers; + Z_attribute_list attr_names; while (len >= 2 + i) { uint16_t attrid = _payload.get16(i); - attr_list.add(attrid); + attr_numbers.add(attrid); + read_attr_ids[i/2] = attrid; // find the attribute name for (uint32_t i = 0; i < ARRAY_SIZE(Z_PostProcess); i++) { @@ -1082,43 +1101,49 @@ void ZCLFrame::parseReadAttributes(JsonObject& json, uint8_t offset) { uint16_t conv_attribute = pgm_read_word(&converter->attribute); if ((conv_cluster == _cluster_id) && (conv_attribute == attrid)) { - attr_names[(const __FlashStringHelper*) (Z_strings + pgm_read_word(&converter->name_offset))] = true; + attr_names.addAttribute(Z_strings + pgm_read_word(&converter->name_offset), true).setBool(true); break; } } i += 2; } + attr_list.addAttribute(F("Read")).setStrRaw(attr_numbers.toString().c_str()); + attr_list.addAttribute(F("ReadNames")).setStrRaw(attr_names.toString(true).c_str()); + + // call auto-responder + Z_AutoResponder(_srcaddr, _cluster_id, _srcendpoint, read_attr_ids, len/2); } // ZCL_CONFIGURE_REPORTING_RESPONSE -void ZCLFrame::parseConfigAttributes(JsonObject& json, uint8_t offset) { - uint32_t i = offset; +void ZCLFrame::parseConfigAttributes(Z_attribute_list& attr_list) { + uint32_t i = 0; uint32_t len = _payload.len(); - - JsonObject &config_rsp = json.createNestedObject(F("ConfigResponse")); uint8_t status = _payload.get8(i); - config_rsp[F("Status")] = status; - config_rsp[F("StatusMsg")] = getZigbeeStatusMessage(status); + + Z_attribute_list attr_config_response; + attr_config_response.addAttribute(F("Status")).setUInt(status); + attr_config_response.addAttribute(F("StatusMsg")).setStr(getZigbeeStatusMessage(status).c_str()); + + Z_attribute &attr_1 = attr_list.addAttribute(F("ConfigResponse")); + attr_1.setStrRaw(attr_config_response.toString(true).c_str()); } // ZCL_READ_REPORTING_CONFIGURATION_RESPONSE -void ZCLFrame::parseReadConfigAttributes(JsonObject& json, uint8_t offset) { - uint32_t i = offset; +void ZCLFrame::parseReadConfigAttributes(Z_attribute_list& attr_list) { + uint32_t i = 0; uint32_t len = _payload.len(); - // json[F(D_CMND_ZIGBEE_CLUSTER)] = _cluster_id; // TODO is it necessary? + Z_attribute &attr_root = attr_list.addAttribute(F("ReadConfig")); + Z_attribute_list attr_1; - JsonObject &attr_names = json.createNestedObject(F("ReadConfig")); while (len >= i + 4) { uint8_t status = _payload.get8(i); uint8_t direction = _payload.get8(i+1); uint16_t attrid = _payload.get16(i+2); - char attr_hex[12]; - snprintf_P(attr_hex, sizeof(attr_hex), "%04X/%04X", _cluster_id, attrid); - JsonObject &attr_details = attr_names.createNestedObject(attr_hex); + Z_attribute_list attr_2; if (direction) { - attr_details[F("DirectionReceived")] = true; + attr_2.addAttribute(F("DirectionReceived")).setBool(true); } // find the attribute name @@ -1128,21 +1153,22 @@ void ZCLFrame::parseReadConfigAttributes(JsonObject& json, uint8_t offset) { uint16_t conv_attribute = pgm_read_word(&converter->attribute); if ((conv_cluster == _cluster_id) && (conv_attribute == attrid)) { - attr_details[(const __FlashStringHelper*) (Z_strings + pgm_read_word(&converter->name_offset))] = true; + const char * attr_name = Z_strings + pgm_read_word(&converter->name_offset); + attr_2.addAttribute(attr_name, true).setBool(true); break; } } i += 4; if (0 != status) { - attr_details[F("Status")] = status; - attr_details[F("StatusMsg")] = getZigbeeStatusMessage(status); + attr_2.addAttribute(F("Status")).setUInt(status); + attr_2.addAttribute(F("StatusMsg")).setStr(getZigbeeStatusMessage(status).c_str()); } else { // no error, decode data if (direction) { // only Timeout period is present uint16_t attr_timeout = _payload.get16(i); i += 2; - attr_details[F("TimeoutPeriod")] = (0xFFFF == attr_timeout) ? -1 : attr_timeout; + attr_2.addAttribute(F("TimeoutPeriod")).setUInt((0xFFFF == attr_timeout) ? -1 : attr_timeout); } else { // direction == 0, we have a data type uint8_t attr_type = _payload.get8(i); @@ -1150,22 +1176,23 @@ void ZCLFrame::parseReadConfigAttributes(JsonObject& json, uint8_t offset) { uint16_t attr_min_interval = _payload.get16(i+1); uint16_t attr_max_interval = _payload.get16(i+3); i += 5; - attr_details[F("MinInterval")] = (0xFFFF == attr_min_interval) ? -1 : attr_min_interval; - attr_details[F("MaxInterval")] = (0xFFFF == attr_max_interval) ? -1 : attr_max_interval; + attr_2.addAttribute(F("MinInterval")).setUInt((0xFFFF == attr_min_interval) ? -1 : attr_min_interval); + attr_2.addAttribute(F("MaxInterval")).setUInt((0xFFFF == attr_max_interval) ? -1 : attr_max_interval); if (!attr_discrete) { // decode Reportable Change - char attr_name[20]; - strcpy_P(attr_name, PSTR("ReportableChange")); - i += parseSingleAttribute(attr_details, attr_name, _payload, i, attr_type); + Z_attribute &attr_change = attr_2.addAttribute(F("ReportableChange")); + i += parseSingleAttribute(attr_change, _payload, i, attr_type); } } } + attr_1.addAttribute(_cluster_id, attrid).setStrRaw(attr_2.toString(true).c_str()); } + attr_root.setStrRaw(attr_1.toString(true).c_str()); } // ZCL_READ_ATTRIBUTES_RESPONSE -void ZCLFrame::parseReadAttributesResponse(JsonObject& json, uint8_t offset) { - uint32_t i = offset; +void ZCLFrame::parseReadAttributesResponse(Z_attribute_list& attr_list) { + uint32_t i = 0; uint32_t len = _payload.len(); while (len >= i + 4) { @@ -1174,10 +1201,8 @@ void ZCLFrame::parseReadAttributesResponse(JsonObject& json, uint8_t offset) { uint8_t status = _payload.get8(i++); if (0 == status) { - char key[16]; - generateAttributeName(json, _cluster_id, attrid, key, sizeof(key)); - - i += parseSingleAttribute(json, key, _payload, i); + Z_attribute & attr = attr_list.addAttribute(_cluster_id, attrid); + i += parseSingleAttribute(attr, _payload, i); } } } @@ -1188,185 +1213,224 @@ void ZCLFrame::parseResponse(void) { uint8_t cmd = _payload.get8(0); uint8_t status = _payload.get8(1); - DynamicJsonBuffer jsonBuffer; - JsonObject& json = jsonBuffer.createObject(); + Z_attribute_list attr_list; // "Device" char s[12]; snprintf_P(s, sizeof(s), PSTR("0x%04X"), _srcaddr); - json[F(D_JSON_ZIGBEE_DEVICE)] = s; + attr_list.addAttribute(F(D_JSON_ZIGBEE_DEVICE)).setStr(s); // "Name" const char * friendlyName = zigbee_devices.getFriendlyName(_srcaddr); if (friendlyName) { - json[F(D_JSON_ZIGBEE_NAME)] = (char*) friendlyName; + attr_list.addAttribute(F(D_JSON_ZIGBEE_NAME)).setStr(friendlyName); } // "Command" snprintf_P(s, sizeof(s), PSTR("%04X!%02X"), _cluster_id, cmd); - json[F(D_JSON_ZIGBEE_CMD)] = s; + attr_list.addAttribute(F(D_JSON_ZIGBEE_CMD)).setStr(s); // "Status" - json[F(D_JSON_ZIGBEE_STATUS)] = status; + attr_list.addAttribute(F(D_JSON_ZIGBEE_STATUS)).setUInt(status); // "StatusMessage" - json[F(D_JSON_ZIGBEE_STATUS_MSG)] = getZigbeeStatusMessage(status); + attr_list.addAttribute(F(D_JSON_ZIGBEE_STATUS_MSG)).setStr(getZigbeeStatusMessage(status).c_str()); // Add Endpoint - json[F(D_CMND_ZIGBEE_ENDPOINT)] = _srcendpoint; + attr_list.addAttribute(F(D_CMND_ZIGBEE_ENDPOINT)).setUInt(_srcendpoint); // Add Group if non-zero - if (_groupaddr) { - json[F(D_CMND_ZIGBEE_GROUP)] = _groupaddr; + if (_groupaddr) { // TODO what about group zero + attr_list.group_id = _groupaddr; } // Add linkquality - json[F(D_CMND_ZIGBEE_LINKQUALITY)] = _linkquality; + attr_list.lqi = _linkquality; - String msg(""); - msg.reserve(100); - json.printTo(msg); - Response_P(PSTR("{\"" D_JSON_ZIGBEE_RESPONSE "\":%s}"), msg.c_str()); + Response_P(PSTR("{\"" D_JSON_ZIGBEE_RESPONSE "\":%s}"), attr_list.toString(true).c_str()); MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR(D_JSON_ZIGBEEZCL_RECEIVED)); } - // Parse non-normalized attributes -void ZCLFrame::parseClusterSpecificCommand(JsonObject& json, uint8_t offset) { - convertClusterSpecific(json, _cluster_id, _cmd_id, _frame_control.b.direction, _srcaddr, _srcendpoint, _payload); +void ZCLFrame::parseClusterSpecificCommand(Z_attribute_list& attr_list) { + convertClusterSpecific(attr_list, _cluster_id, _cmd_id, _frame_control.b.direction, _srcaddr, _srcendpoint, _payload); #ifndef USE_ZIGBEE_NO_READ_ATTRIBUTES // read attributes unless disabled sendHueUpdate(_srcaddr, _groupaddr, _cluster_id, _cmd_id, _frame_control.b.direction); #endif } // ====================================================================== -// Record Manuf -int32_t Z_ManufKeepFunc(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { - zigbee_devices.setManufId(shortaddr, value.as()); - return 1; -} -// Record ModelId -int32_t Z_ModelKeepFunc(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { - zigbee_devices.setModelId(shortaddr, value.as()); - return 1; -} -// Record BatteryPercentage -int32_t Z_BatteryPercentageKeepFunc(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { - zigbee_devices.setBatteryPercent(shortaddr, json[new_name]); - return 1; +// New version of synthetic attribute generation +void ZCLFrame::syntheticAqaraSensor(Z_attribute_list &attr_list, class Z_attribute &attr) { + const SBuffer * buf = attr.getRaw(); + if (buf) { + const SBuffer & buf2 = *buf; + uint32_t i = 0; + uint32_t len = buf2.len(); + + const char * modelId_c = zigbee_devices.getModelId(_srcaddr); // null if unknown + String modelId((char*) modelId_c); + + while (len >= 2 + i) { + uint8_t attrid = buf2.get8(i++); + + Z_attribute attr; // temporary attribute + i += parseSingleAttribute(attr, buf2, i); + int32_t ival32 = attr.getInt(); + float fval = attr.getFloat(); + bool translated = false; // were we able to translate to a known format? + if (0x01 == attrid) { + float batteryvoltage = fval / 100; + attr_list.addAttribute(0x0001, 0x0020).setFloat(batteryvoltage); + uint8_t batterypercentage = toPercentageCR2032(fval); + attr_list.addAttribute(0x0001, 0x0021).setUInt(batterypercentage * 2); + } else if ((nullptr != modelId) && (0 == getManufCode())) { + translated = true; + if (modelId.startsWith(F("lumi.sensor_ht")) || + modelId.startsWith(F("lumi.weather"))) { // Temp sensor + // Filter according to prefix of model name + // onla Aqara Temp/Humidity has manuf_code of zero. If non-zero we skip the parameters + if (0x64 == attrid) { + attr_list.addAttribute(0x0402, 0x0000).setInt(ival32); // Temperature + } else if (0x65 == attrid) { + attr_list.addAttribute(0x0405, 0x0000).setFloat(fval); // Humidity * 100 + } else if (0x66 == attrid) { + attr_list.addAttribute(0x0403, 0x0000).setUInt((ival32 + 50) / 100); // Pressure + } + } else if (modelId.startsWith(F("lumi.sensor_smoke"))) { // gas leak + if (0x64 == attrid) { + attr_list.addAttribute(F("SmokeDensity")).copyVal(attr); + } + } else if (modelId.startsWith(F("lumi.sensor_natgas"))) { // gas leak + if (0x64 == attrid) { + attr_list.addAttribute(F("GasDensity")).copyVal(attr); + } + } else { + translated = false; // we didn't find a match + } + // } else if (0x115F == zcl->getManufCode()) { // Aqara Motion Sensor, still unknown field + } + if (!translated) { + if (attrid >= 100) { // payload is always above 0x64 or 100 + char attr_name[12]; + snprintf_P(attr_name, sizeof(attr_name), PSTR("Xiaomi_%02X"), attrid); + attr_list.addAttribute(attr_name).copyVal(attr); + } + } + } + } } -// Add pressure unit -int32_t Z_AddPressureUnitFunc(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { - json[new_name] = F(D_UNIT_PRESSURE); - return 0; // keep original key +void ZCLFrame::syntheticAqaraSensor2(class Z_attribute_list &attr_list, class Z_attribute &attr) { + const SBuffer * buf = attr.getRaw(); + if (buf) { + const SBuffer & buf2 = *buf; + uint32_t len = buf2.len(); + + // Look for battery value which is the first attribute of type 0x21 + uint16_t struct_size = buf2.get16(0); + size_t struct_len = 2; + if (0xFFFF != struct_size) { + if (struct_size > 16) { struct_size = 16; } + for (uint32_t j = 0; (j < struct_size) && (struct_len < len); j++) { + uint8_t attr_type = buf2.get8(struct_len); + if (0x21 == attr_type) { + uint16_t val = buf2.get16(struct_len+1); + float batteryvoltage = (float)val / 100; + attr_list.addAttribute(0x0001, 0x0020).setFloat(batteryvoltage); + uint8_t batterypercentage = toPercentageCR2032(val); + attr_list.addAttribute(0x0001, 0x0021).setUInt(batterypercentage * 2); + break; + } + struct_len += Z_getDatatypeLen(attr_type) + 1; + } + } + } + attr_list.removeAttribute(&attr); } -// Publish a message for `"Occupancy":0` when the timer expired -int32_t Z_OccupancyCallback(uint16_t shortaddr, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint32_t value) { - DynamicJsonBuffer jsonBuffer; - JsonObject& json = jsonBuffer.createObject(); - json[F(OCCUPANCY)] = 0; - zigbee_devices.jsonPublishNow(shortaddr, json); - return 0; // Fix GCC 10.1 warning -} - -// Aqara Cube -int32_t Z_AqaraCubeFunc(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { - const char * modelId_c = zigbee_devices.findShortAddr(shortaddr).modelId; // null if unknown +// Aqara Cube and Button +void ZCLFrame::syntheticAqaraCubeOrButton(class Z_attribute_list &attr_list, class Z_attribute &attr) { + const char * modelId_c = zigbee_devices.findShortAddr(_srcaddr).modelId; // null if unknown String modelId((char*) modelId_c); if (modelId.startsWith(F("lumi.sensor_cube"))) { // only for Aqara cube - int32_t val = value; + int32_t val = attr.getInt(); const __FlashStringHelper *aqara_cube = F("AqaraCube"); const __FlashStringHelper *aqara_cube_side = F("AqaraCubeSide"); const __FlashStringHelper *aqara_cube_from_side = F("AqaraCubeFromSide"); switch (val) { case 0: - json[aqara_cube] = F("shake"); + attr_list.addAttribute(aqara_cube).setStr(PSTR("shake")); break; case 2: - json[aqara_cube] = F("wakeup"); + attr_list.addAttribute(aqara_cube).setStr(PSTR("wakeup")); break; case 3: - json[aqara_cube] = F("fall"); + attr_list.addAttribute(aqara_cube).setStr(PSTR("fall")); break; case 64 ... 127: - json[aqara_cube] = F("flip90"); - json[aqara_cube_side] = val % 8; - json[aqara_cube_from_side] = (val - 64) / 8; + attr_list.addAttribute(aqara_cube).setStr(PSTR("flip90")); + attr_list.addAttribute(aqara_cube_side).setInt(val % 8); + attr_list.addAttribute(aqara_cube_from_side).setInt((val - 64) / 8); break; case 128 ... 132: - json[aqara_cube] = F("flip180"); - json[aqara_cube_side] = val - 128; + attr_list.addAttribute(aqara_cube).setStr(PSTR("flip180")); + attr_list.addAttribute(aqara_cube_side).setInt(val - 128); break; case 256 ... 261: - json[aqara_cube] = F("slide"); - json[aqara_cube_side] = val - 256; + attr_list.addAttribute(aqara_cube).setStr(PSTR("slide")); + attr_list.addAttribute(aqara_cube_side).setInt(val - 256); break; case 512 ... 517: - json[aqara_cube] = F("tap"); - json[aqara_cube_side] = val - 512; + attr_list.addAttribute(aqara_cube).setStr(PSTR("tap")); + attr_list.addAttribute(aqara_cube_side).setInt(val - 512); break; } - return 1; - } - - // Source: https://github.com/kirovilya/ioBroker.zigbee - // +---+ - // | 2 | - // +---+---+---+ - // | 4 | 0 | 1 | - // +---+---+---+ - // |M5I| - // +---+ - // | 3 | - // +---+ - // Side 5 is with the MI logo, side 3 contains the battery door. - // presentValue = 0 = shake - // presentValue = 2 = wakeup - // presentValue = 3 = fly/fall - // presentValue = y + x * 8 + 64 = 90º Flip from side x on top to side y on top - // presentValue = x + 128 = 180º flip to side x on top - // presentValue = x + 256 = push/slide cube while side x is on top - // presentValue = x + 512 = double tap while side x is on top - return 0; -} - -// Aqara Button -int32_t Z_AqaraButtonFunc(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { - const char * modelId_c = zigbee_devices.getModelId(shortaddr); // null if unknown - String modelId((char*) modelId_c); - - if (modelId.startsWith(F("lumi.remote"))) { // only for Aqara button - int32_t val = value; + attr_list.removeAttribute(&attr); + // Source: https://github.com/kirovilya/ioBroker.zigbee + // +---+ + // | 2 | + // +---+---+---+ + // | 4 | 0 | 1 | + // +---+---+---+ + // |M5I| + // +---+ + // | 3 | + // +---+ + // Side 5 is with the MI logo, side 3 contains the battery door. + // presentValue = 0 = shake + // presentValue = 2 = wakeup + // presentValue = 3 = fly/fall + // presentValue = y + x * 8 + 64 = 90º Flip from side x on top to side y on top + // presentValue = x + 128 = 180º flip to side x on top + // presentValue = x + 256 = push/slide cube while side x is on top + // presentValue = x + 512 = double tap while side x is on top + } else if (modelId.startsWith(F("lumi.remote"))) { // only for Aqara button + int32_t val = attr.getInt(); const __FlashStringHelper *aqara_click = F("click"); const __FlashStringHelper *aqara_action = F("action"); switch (val) { case 0: - json[aqara_action] = F("hold"); + attr_list.addAttribute(aqara_action).setStr(PSTR("hold")); break; case 1: - json[aqara_click] = F("single"); + attr_list.addAttribute(aqara_click).setStr(PSTR("single")); break; case 2: - json[aqara_click] = F("double"); + attr_list.addAttribute(aqara_click).setStr(PSTR("double")); break; case 255: - json[aqara_action] = F("release"); + attr_list.addAttribute(aqara_click).setStr(PSTR("release")); break; default: - json[aqara_action] = val; + attr_list.addAttribute(aqara_click).setUInt(val); break; } - return 1; } - - return 0; } -// Aqara Vibration Sensor - special proprietary attributes -int32_t Z_AqaraVibrationFunc(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { - //json[new_name] = value; - switch (attr) { +// Aqara vibration device +void ZCLFrame::syntheticAqaraVibration(class Z_attribute_list &attr_list, class Z_attribute &attr) { + switch (attr.key.id.attr_id) { case 0x0055: { - int32_t ivalue = value; + int32_t ivalue = attr.getInt(); const __FlashStringHelper * svalue; switch (ivalue) { case 1: svalue = F("vibrate"); break; @@ -1374,283 +1438,123 @@ int32_t Z_AqaraVibrationFunc(const class ZCLFrame *zcl, uint16_t shortaddr, Json case 3: svalue = F("drop"); break; default: svalue = F("unknown"); break; } - json[new_name] = svalue; + attr.setStr((const char*)svalue); } break; - // case 0x0503: - // break; - // case 0x0505: - // break; + case 0x0503: + break; + case 0x0505: + break; case 0x0508: { // see https://github.com/Koenkk/zigbee2mqtt/issues/295 and http://faire-ca-soi-meme.fr/domotique/2018/09/03/test-xiaomi-aqara-vibration-sensor/ // report accelerometer measures - String hex = value; - SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); - int16_t x, y, z; - z = buf2.get16(0); - y = buf2.get16(2); - x = buf2.get16(4); - JsonArray& xyz = json.createNestedArray(new_name); - xyz.add(x); - xyz.add(y); - xyz.add(z); - // calculate angles - float X = x; - float Y = y; - float Z = z; - int32_t Angle_X = 0.5f + atanf(X/sqrtf(z*z+y*y)) * f_180pi; - int32_t Angle_Y = 0.5f + atanf(Y/sqrtf(x*x+z*z)) * f_180pi; - int32_t Angle_Z = 0.5f + atanf(Z/sqrtf(x*x+y*y)) * f_180pi; - JsonArray& angles = json.createNestedArray(F("AqaraAngles")); - angles.add(Angle_X); - angles.add(Angle_Y); - angles.add(Angle_Z); + const SBuffer * buf = attr.getRaw(); + if (buf) { + const SBuffer & buf2 = *buf; + int16_t x, y, z; + z = buf2.get16(0); + y = buf2.get16(2); + x = buf2.get16(4); + char temp[32]; + snprintf_P(temp, sizeof(temp), "[%i,%i,%i]", x, y, z); + attr.setStrRaw(temp); + // calculate angles + float X = x; + float Y = y; + float Z = z; + int32_t Angle_X = 0.5f + atanf(X/sqrtf(z*z+y*y)) * f_180pi; + int32_t Angle_Y = 0.5f + atanf(Y/sqrtf(x*x+z*z)) * f_180pi; + int32_t Angle_Z = 0.5f + atanf(Z/sqrtf(x*x+y*y)) * f_180pi; + snprintf_P(temp, sizeof(temp), "[%i,%i,%i]", Angle_X, Angle_Y, Angle_Z); + attr_list.addAttribute(F("AqaraAngles")).setStrRaw(temp); + } } break; } - return 1; // remove original key } -int32_t Z_AqaraSensorFunc(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { - String hex = value; - SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); - uint32_t i = 0; - uint32_t len = buf2.len(); - char tmp[] = "tmp"; // for obscure reasons, it must be converted from const char* to char*, otherwise ArduinoJson gets confused - - const char * modelId_c = zigbee_devices.getModelId(shortaddr); // null if unknown - String modelId((char*) modelId_c); - - while (len >= 2 + i) { - uint8_t attrid = buf2.get8(i++); - - i += parseSingleAttribute(json, tmp, buf2, i); - float val = json[tmp]; - json.remove(tmp); - bool translated = false; // were we able to translate to a known format? - if (0x01 == attrid) { - float batteryvoltage = val / 1000.0f; - json[F("BatteryVoltage")] = batteryvoltage; - uint8_t batterypercentage = toPercentageCR2032(val); - json[F("BatteryPercentage")] = batterypercentage; - zigbee_devices.setBatteryPercent(shortaddr, batterypercentage); - // deprecated - json[F(D_JSON_VOLTAGE)] = batteryvoltage; - json[F("Battery")] = toPercentageCR2032(val); - } else if ((nullptr != modelId) && (0 == zcl->getManufCode())) { - translated = true; - if (modelId.startsWith(F("lumi.sensor_ht")) || - modelId.startsWith(F("lumi.weather"))) { // Temp sensor - // Filter according to prefix of model name - // onla Aqara Temp/Humidity has manuf_code of zero. If non-zero we skip the parameters - if (0x64 == attrid) { - json[F(D_JSON_TEMPERATURE)] = val / 100.0f; - } else if (0x65 == attrid) { - json[F(D_JSON_HUMIDITY)] = val / 100.0f; - } else if (0x66 == attrid) { - json[F(D_JSON_PRESSURE)] = val / 100.0f; - json[F(D_JSON_PRESSURE_UNIT)] = F(D_UNIT_PRESSURE); // hPa - } - } else if (modelId.startsWith(F("lumi.sensor_smoke"))) { // gas leak - if (0x64 == attrid) { - json[F("SmokeDensity")] = val; - } - } else if (modelId.startsWith(F("lumi.sensor_natgas"))) { // gas leak - if (0x64 == attrid) { - json[F("GasDensity")] = val; - } - } else { - translated = false; // we didn't find a match - } - // } else if (0x115F == zcl->getManufCode()) { // Aqara Motion Sensor, still unknown field - } - if (!translated) { - if (attrid >= 100) { // payload is always above 0x64 or 100 - char attr_name[12]; - snprintf_P(attr_name, sizeof(attr_name), PSTR("Xiaomi_%02X"), attrid); - json[attr_name] = val; - } - } - } - return 1; // remove original key +/// Publish a message for `"Occupancy":0` when the timer expired +int32_t Z_OccupancyCallback(uint16_t shortaddr, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint32_t value) { + Z_attribute_list attr_list; + attr_list.addAttribute(F(OCCUPANCY)).setUInt(0); + zigbee_devices.jsonPublishNow(shortaddr, attr_list); + return 0; // Fix GCC 10.1 warning } -int32_t Z_AqaraSensorFunc2(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { - String hex = value; - SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); - uint32_t i = 0; - uint32_t len = buf2.len(); - - // Look for battery value which is the first attribute of type 0x21 - uint16_t struct_size = buf2.get16(0); - size_t struct_len = 2; - if (0xFFFF != struct_size) { - if (struct_size > 16) { struct_size = 16; } - for (uint32_t j = 0; (j < struct_size) && (struct_len < len); j++) { - uint8_t attr_type = buf2.get8(struct_len); - if (0x21 == attr_type) { - uint16_t val = buf2.get16(struct_len+1); - float batteryvoltage = val / 1000.0f; - json[F("BatteryVoltage")] = batteryvoltage; - uint8_t batterypercentage = toPercentageCR2032(val); - json[F("BatteryPercentage")] = batterypercentage; - zigbee_devices.setBatteryPercent(shortaddr, batterypercentage); - break; - } - struct_len += Z_getDatatypeLen(attr_type) + 1; - } - } - - return 0; // remove original key -} // ====================================================================== - -// apply the transformation from the converter -int32_t Z_ApplyConverter(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, - uint16_t cluster, uint16_t attr, int8_t multiplier, uint8_t cb) { - // apply multiplier if needed - if (1 == multiplier) { // copy unchanged - json[new_name] = value; - } else if (0 != multiplier) { - if (multiplier > 0) { - json[new_name] = ((float)value) * multiplier; - } else { - json[new_name] = ((float)value) / (-multiplier); - } - } - - // apply callback if needed - Z_AttrConverter func = nullptr; - switch (cb) { - case Z_Nop: - return 1; // drop original key - case Z_AddPressureUnit: - func = &Z_AddPressureUnitFunc; - break; - case Z_ManufKeep: - func = &Z_ManufKeepFunc; - break; - case Z_ModelKeep: - func = &Z_ModelKeepFunc; - break; - case Z_AqaraSensor: - func = &Z_AqaraSensorFunc; - break; - case Z_AqaraSensor2: - func = &Z_AqaraSensorFunc2; - break; - case Z_AqaraVibration: - func = &Z_AqaraVibrationFunc; - break; - case Z_AqaraCube: - func = &Z_AqaraCubeFunc; - break; - case Z_AqaraButton: - func = &Z_AqaraButtonFunc; - break; - case Z_BatteryPercentage: - func = &Z_BatteryPercentageKeepFunc; - break; - }; - - if (func) { - return (*func)(zcl, shortaddr, json, name, value, new_name, cluster, attr); - } - return 1; // Fix GCC 10.1 warning -} - -// Scan all the final attributes and update any internal representation like sensors -void ZCLFrame::updateInternalAttributes(uint16_t shortaddr, JsonObject& json) { - Z_Device & device = zigbee_devices.getShortAddr(shortaddr); - for (auto kv : json) { - String key_string = kv.key; - const char * key = key_string.c_str(); - JsonVariant& value = kv.value; - - if (key_string.equalsIgnoreCase(F("Temperature"))) { - device.temperature = value.as() * 10 + 0.5f; - } else if (key_string.equalsIgnoreCase(F("Humidity"))) { - device.humidity = value.as() + 0.5f; - } else if (key_string.equalsIgnoreCase(F("Pressure"))) { - device.pressure = value.as() + 0.5f; - } - } -} - -void ZCLFrame::postProcessAttributes(uint16_t shortaddr, JsonObject& json) { +void ZCLFrame::postProcessAttributes(uint16_t shortaddr, Z_attribute_list& attr_list) { // source endpoint uint8_t src_ep = _srcendpoint; - // iterate on json elements - for (auto kv : json) { - String key_string = kv.key; - const char * key = key_string.c_str(); - JsonVariant& value = kv.value; - // Check that format looks like "CCCC/AAAA" or "CCCC/AAAA+d" - char * delimiter = strchr(key, '/'); - char * delimiter2 = strchr(key, '+'); - if (delimiter) { - uint16_t attribute; - uint16_t suffix = 1; - uint16_t cluster = strtoul(key, &delimiter, 16); - if (!delimiter2) { - attribute = strtoul(delimiter+1, nullptr, 16); - } else { - attribute = strtoul(delimiter+1, &delimiter2, 16); - suffix = strtoul(delimiter2+1, nullptr, 10); - } - + + for (auto &attr : attr_list) { + // attr is Z_attribute& + if (!attr.key_is_str) { + uint16_t cluster = attr.key.id.cluster; + uint16_t attribute = attr.key.id.attr_id; + uint32_t ccccaaaa = (attr.key.id.cluster << 16) | attr.key.id.attr_id; Z_Device & device = zigbee_devices.getShortAddr(shortaddr); - uint16_t uval16 = value; // call converter from JSonVariant to int only once - int16_t ival16 = value; // call converter from JSonVariant to int only once - // see if we need to update the Hue bulb status - if ((cluster == 0x0006) && ((attribute == 0x0000) || (attribute == 0x8000))) { - bool power = value; - device.setPower(power); - } else if ((cluster == 0x0008) && (attribute == 0x0000)) { - device.dimmer = uval16; - } else if ((cluster == 0x0300) && (attribute == 0x0000)) { - device.hue = changeUIntScale(uval16, 0, 254, 0, 360); // change range from 0..254 to 0..360 - } else if ((cluster == 0x0300) && (attribute == 0x0001)) { - device.sat = uval16; - } else if ((cluster == 0x0300) && (attribute == 0x0003)) { - device.x = uval16; - } else if ((cluster == 0x0300) && (attribute == 0x0004)) { - device.y = uval16; - } else if ((cluster == 0x0300) && (attribute == 0x0007)) { - device.ct = uval16; - } else if ((cluster == 0x0300) && (attribute == 0x0008)) { - device.colormode = uval16; - } else if ((cluster == 0x0B04) && (attribute == 0x0505)) { - device.mains_voltage = uval16; - } else if ((cluster == 0x0B04) && (attribute == 0x050B)) { - device.mains_power = ival16; - } - // Iterate on filter + // Look for an entry in the converter table + bool found = false; + int8_t conv_multiplier; + const char * conv_name; for (uint32_t i = 0; i < ARRAY_SIZE(Z_PostProcess); i++) { const Z_AttributeConverter *converter = &Z_PostProcess[i]; uint16_t conv_cluster = CxToCluster(pgm_read_byte(&converter->cluster_short)); uint16_t conv_attribute = pgm_read_word(&converter->attribute); - int8_t conv_multiplier = pgm_read_byte(&converter->multiplier); - uint8_t conv_cb = pgm_read_byte(&converter->cb); // callback id if ((conv_cluster == cluster) && ((conv_attribute == attribute) || (conv_attribute == 0xFFFF)) ) { - String new_name_str = (const __FlashStringHelper*) (Z_strings + pgm_read_word(&converter->name_offset)); - if (suffix > 1) { new_name_str += suffix; } // append suffix number - // apply the transformation - int32_t drop = Z_ApplyConverter(this, shortaddr, json, key, value, new_name_str, conv_cluster, conv_attribute, conv_multiplier, conv_cb); - if (drop) { - json.remove(key); - } + conv_multiplier = pgm_read_byte(&converter->multiplier); + conv_name = Z_strings + pgm_read_word(&converter->name_offset); + found = true; + break; + } + } + // apply multiplier if needed + float fval = attr.getFloat(); + if (found) { + if (0 == conv_multiplier) { attr_list.removeAttribute(&attr); continue; } // remove attribute if multiplier is zero + if (1 != conv_multiplier) { + if (conv_multiplier > 0) { fval = fval * conv_multiplier; } + else { fval = fval / (-conv_multiplier); } + attr.setFloat(fval); + } + } + + uint16_t uval16 = attr.getUInt(); // call converter to uint only once + int16_t ival16 = attr.getInt(); // call converter to int only once + // update any internal structure + switch (ccccaaaa) { + case 0x00000004: zigbee_devices.setManufId(shortaddr, attr.getStr()); break; + case 0x00000005: zigbee_devices.setModelId(shortaddr, attr.getStr()); break; + case 0x00010021: zigbee_devices.setBatteryPercent(shortaddr, uval16); break; + case 0x00060000: + case 0x00068000: device.setPower(attr.getBool()); break; + case 0x00080000: device.dimmer = uval16; break; + case 0x03000000: device.hue = changeUIntScale(uval16, 0, 254, 0, 360); break; + case 0x03000001: device.sat = uval16; break; + case 0x03000003: device.x = uval16; break; + case 0x03000004: device.y = uval16; break; + case 0x03000007: device.ct = uval16; break; + case 0x03000008: device.colormode = uval16; break; + case 0x04020000: device.temperature = fval * 10 + 0.5f; break; + case 0x04030000: device.pressure = fval + 0.5f; break; + case 0x04050000: device.humidity = fval + 0.5f; break; + case 0x0B040505: device.mains_voltage = uval16; break; + case 0x0B04050B: device.mains_power = ival16; break; + } + + // Replace cluster/attribute with name + if (found) { + if (0x00 != pgm_read_byte(conv_name)) {// if name is not null, replace it + attr.setKeyName(conv_name, true); // PMEM string so no need to copy } } } } - - updateInternalAttributes(shortaddr, json); } #endif // USE_ZIGBEE diff --git a/tasmota/xdrv_23_zigbee_6_commands.ino b/tasmota/xdrv_23_zigbee_6_commands.ino index 1a3e5b103..c533eb8ed 100644 --- a/tasmota/xdrv_23_zigbee_6_commands.ino +++ b/tasmota/xdrv_23_zigbee_6_commands.ino @@ -328,13 +328,8 @@ void sendHueUpdate(uint16_t shortaddr, uint16_t groupaddr, uint16_t cluster, uin // Parse a cluster specific command, and try to convert into human readable -void convertClusterSpecific(JsonObject& json, uint16_t cluster, uint8_t cmd, bool direction, uint16_t shortaddr, uint8_t srcendpoint, const SBuffer &payload) { - size_t hex_char_len = payload.len()*2+2; - char *hex_char = (char*) malloc(hex_char_len); - if (!hex_char) { return; } - ToHex_P((unsigned char*)payload.getBuffer(), payload.len(), hex_char, hex_char_len); - - const __FlashStringHelper* command_name = nullptr; +void convertClusterSpecific(class Z_attribute_list &attr_list, uint16_t cluster, uint8_t cmd, bool direction, uint16_t shortaddr, uint8_t srcendpoint, const SBuffer &payload) { + const char * command_name = nullptr; uint8_t conv_direction; Z_XYZ_Var xyz; @@ -373,7 +368,7 @@ void convertClusterSpecific(JsonObject& json, uint16_t cluster, uint8_t cmd, boo p += 2; } if (match) { - command_name = (const __FlashStringHelper*) (Z_strings + pgm_read_word(&conv->tasmota_cmd_offset)); + command_name = Z_strings + pgm_read_word(&conv->tasmota_cmd_offset); parseXYZ(Z_strings + pgm_read_word(&conv->param_offset), payload, &xyz); if (0xFF == conv_cmd) { // shift all values @@ -396,112 +391,105 @@ void convertClusterSpecific(JsonObject& json, uint16_t cluster, uint8_t cmd, boo // Format: "0004<00": "00" = "<": "" for commands to devices char attrid_str[12]; snprintf_P(attrid_str, sizeof(attrid_str), PSTR("%04X%c%02X"), cluster, direction ? '<' : '!', cmd); - json[attrid_str] = hex_char; - free(hex_char); + attr_list.addAttribute(attrid_str).setBuf(payload, 0, payload.len()); if (command_name) { // Now try to transform into a human readable format - String command_name2 = String(command_name); // if (direction & 0x80) then specific transform if (conv_direction & 0x80) { - // TODO need to create a specific command + uint32_t cccc00mm = (cluster << 16) | cmd; // format = cccc00mm, cccc = cluster, mm = command // IAS - if ((cluster == 0x0500) && (cmd == 0x00)) { - // "ZoneStatusChange" - json[command_name] = xyz.x; + switch (cccc00mm) { + case 0x05000000: // "ZoneStatusChange" + attr_list.addAttribute(command_name, true).setUInt(xyz.x); if (0 != xyz.y) { - json[command_name2 + F("Ext")] = xyz.y; + attr_list.addAttribute(command_name, PSTR("Ext")).setUInt(xyz.y); } if ((0 != xyz.z) && (0xFF != xyz.z)) { - json[command_name2 + F("Zone")] = xyz.z; + attr_list.addAttribute(command_name, PSTR("Zone")).setUInt(xyz.z); } - } else if ((cluster == 0x0004) && ((cmd == 0x00) || (cmd == 0x01) || (cmd == 0x03))) { - // AddGroupResp or ViewGroupResp (group name ignored) or RemoveGroup - json[command_name] = xyz.y; - json[command_name2 + F("Status")] = xyz.x; - json[command_name2 + F("StatusMsg")] = getZigbeeStatusMessage(xyz.x); - } else if ((cluster == 0x0004) && (cmd == 0x02)) { - // GetGroupResp - json[command_name2 + F("Capacity")] = xyz.x; - json[command_name2 + F("Count")] = xyz.y; - JsonArray &arr = json.createNestedArray(command_name); - for (uint32_t i = 0; i < xyz.y; i++) { - arr.add(payload.get16(2 + 2*i)); + break; + case 0x00040000: + case 0x00040001: + case 0x00040003: // AddGroupResp or ViewGroupResp (group name ignored) or RemoveGroup + attr_list.addAttribute(command_name, true).setUInt(xyz.y); + attr_list.addAttribute(command_name, PSTR("Status")).setUInt(xyz.x); + attr_list.addAttribute(command_name, PSTR("StatusMsg")).setStr(getZigbeeStatusMessage(xyz.x).c_str()); + break; + case 0x00040002: // GetGroupResp + attr_list.addAttribute(command_name, PSTR("Capacity")).setUInt(xyz.x); + attr_list.addAttribute(command_name, PSTR("Count")).setUInt(xyz.y); + { + + Z_json_array group_list; + for (uint32_t i = 0; i < xyz.y; i++) { + group_list.add(payload.get16(2 + 2*i)); + } + attr_list.addAttribute(command_name, true).setStrRaw(group_list.toString().c_str()); } - } else if ((cluster == 0x0005) && ((cmd == 0x00) || (cmd == 0x02) || (cmd == 0x03))) { - // AddScene or RemoveScene or StoreScene - json[command_name2 + F("Status")] = xyz.x; - json[command_name2 + F("StatusMsg")] = getZigbeeStatusMessage(xyz.x); - json[F("GroupId")] = xyz.y; - json[F("SceneId")] = xyz.z; - } else if ((cluster == 0x0005) && (cmd == 0x01)) { - // ViewScene - json[command_name2 + F("Status")] = xyz.x; - json[command_name2 + F("StatusMsg")] = getZigbeeStatusMessage(xyz.x); - json[F("GroupId")] = xyz.y; - json[F("SceneId")] = xyz.z; - String scene_payload = json[attrid_str]; - json[F("ScenePayload")] = scene_payload.substring(8); // remove first 8 characters - } else if ((cluster == 0x0005) && (cmd == 0x03)) { - // RemoveAllScenes - json[command_name2 + F("Status")] = xyz.x; - json[command_name2 + F("StatusMsg")] = getZigbeeStatusMessage(xyz.x); - json[F("GroupId")] = xyz.y; - } else if ((cluster == 0x0005) && (cmd == 0x06)) { - // GetSceneMembership - json[command_name2 + F("Status")] = xyz.x; - json[command_name2 + F("StatusMsg")] = getZigbeeStatusMessage(xyz.x); - json[F("Capacity")] = xyz.y; - json[F("GroupId")] = xyz.z; - String scene_payload = json[attrid_str]; - json[F("ScenePayload")] = scene_payload.substring(8); // remove first 8 characters - } else if ((cluster == 0x0006) && (cmd == 0x40)) { - // Power Off With Effect - json[F("Power")] = 0; // always "Power":0 - json[F("PowerEffect")] = xyz.x; - json[F("PowerEffectVariant")] = xyz.y; - } else if ((cluster == 0x0006) && (cmd == 0x41)) { - // Power On With Recall Global Scene - json[F("Power")] = 1; // always "Power":1 - json[F("PowerRecallGlobalScene")] = true; - } else if ((cluster == 0x0006) && (cmd == 0x42)) { - // Power On With Timed Off Command - json[F("Power")] = 1; // always "Power":1 - json[F("PowerOnlyWhenOn")] = xyz.x; - json[F("PowerOnTime")] = xyz.y / 10.0f; - json[F("PowerOffWait")] = xyz.z / 10.0f; + break; + case 0x00050000: + case 0x00050001: // ViewScene + case 0x00050002: + case 0x00050004: // AddScene or RemoveScene or StoreScene + attr_list.addAttribute(command_name, PSTR("Status")).setUInt(xyz.x); + attr_list.addAttribute(command_name, PSTR("StatusMsg")).setStr(getZigbeeStatusMessage(xyz.x).c_str()); + attr_list.addAttribute(PSTR("GroupId"), true).setUInt(xyz.y); + attr_list.addAttribute(PSTR("SceneId"), true).setUInt(xyz.z); + if (0x00050001 == cccc00mm) { // ViewScene specific + attr_list.addAttribute(PSTR("ScenePayload"), true).setBuf(payload, 4, payload.len()-4); // remove first 4 bytes + } + break; + case 0x00050003: // RemoveAllScenes + attr_list.addAttribute(command_name, PSTR("Status")).setUInt(xyz.x); + attr_list.addAttribute(command_name, PSTR("StatusMsg")).setStr(getZigbeeStatusMessage(xyz.x).c_str()); + attr_list.addAttribute(PSTR("GroupId"), true).setUInt(xyz.y); + break; + case 0x00050006: // GetSceneMembership + attr_list.addAttribute(command_name, PSTR("Status")).setUInt(xyz.x); + attr_list.addAttribute(command_name, PSTR("StatusMsg")).setStr(getZigbeeStatusMessage(xyz.x).c_str()); + attr_list.addAttribute(PSTR("Capacity"), true).setUInt(xyz.y); + attr_list.addAttribute(PSTR("GroupId"), true).setUInt(xyz.z); + attr_list.addAttribute(PSTR("ScenePayload"), true).setBuf(payload, 4, payload.len()-4); // remove first 4 bytes + break; + case 0x00060040: // Power Off With Effect + attr_list.addAttribute(PSTR("Power"), true).setUInt(0); + attr_list.addAttribute(PSTR("PowerEffect"), true).setUInt(xyz.x); + attr_list.addAttribute(PSTR("PowerEffectVariant"), true).setUInt(xyz.y); + break; + case 0x00060041: // Power On With Recall Global Scene + attr_list.addAttribute(PSTR("Power"), true).setUInt(1); + attr_list.addAttribute(PSTR("PowerRecallGlobalScene"), true).setBool(true); + break; + case 0x00060042: // Power On With Timed Off Command + attr_list.addAttribute(PSTR("Power"), true).setUInt(1); + attr_list.addAttribute(PSTR("PowerOnlyWhenOn"), true).setUInt(xyz.x); + attr_list.addAttribute(PSTR("PowerOnTime"), true).setFloat(xyz.y / 10.0f); + attr_list.addAttribute(PSTR("PowerOffWait"), true).setFloat(xyz.z / 10.0f); + break; } } else { // general case - bool extended_command = false; // do we send command with endpoint suffix + // do we send command with endpoint suffix + char command_suffix[4] = { 0x00 }; // empty string by default // if SO101 and multiple endpoints, append endpoint number if (Settings.flag4.zb_index_ep) { if (zigbee_devices.countEndpoints(shortaddr) > 0) { - command_name2 += srcendpoint; - extended_command = true; + snprintf_P(command_suffix, sizeof(command_suffix), PSTR("%d"), srcendpoint); } } if (0 == xyz.x_type) { - json[command_name] = true; // no parameter - if (extended_command) { json[command_name2] = true; } + attr_list.addAttribute(command_name, command_suffix).setBool(true); } else if (0 == xyz.y_type) { - json[command_name] = xyz.x; // 1 parameter - if (extended_command) { json[command_name2] = xyz.x; } + attr_list.addAttribute(command_name, command_suffix).setUInt(xyz.x); } else { // multiple answers, create an array - JsonArray &arr = json.createNestedArray(command_name); + Z_json_array arr; arr.add(xyz.x); arr.add(xyz.y); if (xyz.z_type) { arr.add(xyz.z); } - if (extended_command) { - JsonArray &arr = json.createNestedArray(command_name2); - arr.add(xyz.x); - arr.add(xyz.y); - if (xyz.z_type) { - arr.add(xyz.z); - } - } + attr_list.addAttribute(command_name, command_suffix).setStrRaw(arr.toString().c_str()); } } } diff --git a/tasmota/xdrv_23_zigbee_8_parsers.ino b/tasmota/xdrv_23_zigbee_8_parsers.ino index 67eafb8b0..1f5d33a8f 100644 --- a/tasmota/xdrv_23_zigbee_8_parsers.ino +++ b/tasmota/xdrv_23_zigbee_8_parsers.ino @@ -1011,53 +1011,53 @@ int32_t EZ_ReceiveTCJoinHandler(int32_t res, const class SBuffer &buf) { // Parse incoming ZCL message. // // This code is common to ZNP and EZSP -void Z_IncomingMessage(ZCLFrame &zcl_received) { +void Z_IncomingMessage(class ZCLFrame &zcl_received) { uint16_t srcaddr = zcl_received.getSrcAddr(); uint16_t groupid = zcl_received.getGroupAddr(); uint16_t clusterid = zcl_received.getClusterId(); uint8_t linkquality = zcl_received.getLinkQuality(); uint8_t srcendpoint = zcl_received.getSrcEndpoint(); + linkquality = linkquality != 0xFF ? linkquality : 0xFE; // avoid 0xFF (reserved for unknown) bool defer_attributes = false; // do we defer attributes reporting to coalesce // log the packet details zcl_received.log(); - zigbee_devices.setLQI(srcaddr, linkquality != 0xFF ? linkquality : 0xFE); // EFR32 has a different scale for LQI + zigbee_devices.setLQI(srcaddr, linkquality); // EFR32 has a different scale for LQI char shortaddr[8]; snprintf_P(shortaddr, sizeof(shortaddr), PSTR("0x%04X"), srcaddr); - DynamicJsonBuffer jsonBuffer; - JsonObject& json = jsonBuffer.createObject(); + Z_attribute_list attr_list; + attr_list.lqi = linkquality; + attr_list.src_ep = srcendpoint; + if (groupid) { // TODO we miss the group_id == 0 here + attr_list.group_id = groupid; + } if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_DEFAULT_RESPONSE == zcl_received.getCmdId())) { - zcl_received.parseResponse(); // Zigbee general "Degault Response", publish ZbResponse message + zcl_received.parseResponse(); // Zigbee general "Default Response", publish ZbResponse message } else { - // Build the ZbReceive json + // Build the ZbReceive list if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_REPORT_ATTRIBUTES == zcl_received.getCmdId())) { - zcl_received.parseReportAttributes(json); // Zigbee report attributes from sensors + zcl_received.parseReportAttributes(attr_list); // Zigbee report attributes from sensors if (clusterid) { defer_attributes = true; } // don't defer system Cluster=0 messages } else if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_READ_ATTRIBUTES_RESPONSE == zcl_received.getCmdId())) { - zcl_received.parseReadAttributesResponse(json); + zcl_received.parseReadAttributesResponse(attr_list); if (clusterid) { defer_attributes = true; } // don't defer system Cluster=0 messages } else if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_READ_ATTRIBUTES == zcl_received.getCmdId())) { - zcl_received.parseReadAttributes(json); + zcl_received.parseReadAttributes(attr_list); // never defer read_attributes, so the auto-responder can send response back on a per cluster basis } else if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_READ_REPORTING_CONFIGURATION_RESPONSE == zcl_received.getCmdId())) { - zcl_received.parseReadConfigAttributes(json); + zcl_received.parseReadConfigAttributes(attr_list); } else if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_CONFIGURE_REPORTING_RESPONSE == zcl_received.getCmdId())) { - zcl_received.parseConfigAttributes(json); + zcl_received.parseConfigAttributes(attr_list); } else if (zcl_received.isClusterSpecificCommand()) { - zcl_received.parseClusterSpecificCommand(json); + zcl_received.parseClusterSpecificCommand(attr_list); } - { // fence to force early de-allocation of msg - String msg(""); - msg.reserve(100); - json.printTo(msg); - AddLog_P2(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE D_JSON_ZIGBEEZCL_RAW_RECEIVED ": {\"0x%04X\":%s}"), srcaddr, msg.c_str()); - } + AddLog_P2(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE D_JSON_ZIGBEEZCL_RAW_RECEIVED ": {\"0x%04X\":{%s}}"), srcaddr, attr_list.toString().c_str()); // discard the message if it was sent by us (broadcast or group loopback) if (srcaddr == localShortAddr) { @@ -1065,37 +1065,25 @@ void Z_IncomingMessage(ZCLFrame &zcl_received) { return; // abort the rest of message management } - zcl_received.postProcessAttributes(srcaddr, json); - // Add Endpoint - json[F(D_CMND_ZIGBEE_ENDPOINT)] = srcendpoint; - // Add Group if non-zero - if (groupid) { - json[F(D_CMND_ZIGBEE_GROUP)] = groupid; - } - // Add linkquality - json[F(D_CMND_ZIGBEE_LINKQUALITY)] = linkquality; + zcl_received.generateSyntheticAttributes(attr_list); + zcl_received.generateCallBacks(attr_list); // set deferred callbacks, ex: Occupancy + zcl_received.postProcessAttributes(srcaddr, attr_list); // since we just receveived data from the device, it is reachable zigbee_devices.resetTimersForDevice(srcaddr, 0 /* groupaddr */, Z_CAT_REACHABILITY); // remove any reachability timer already there zigbee_devices.setReachable(srcaddr, true); // mark device as reachable - // Post-provess for Aqara Presence Senson - Z_AqaraOccupancy(srcaddr, clusterid, srcendpoint, json); - if (defer_attributes) { // Prepare for publish - if (zigbee_devices.jsonIsConflict(srcaddr, json)) { + if (zigbee_devices.jsonIsConflict(srcaddr, attr_list)) { // there is conflicting values, force a publish of the previous message now and don't coalesce zigbee_devices.jsonPublishFlush(srcaddr); } - zigbee_devices.jsonAppend(srcaddr, json); + zigbee_devices.jsonAppend(srcaddr, attr_list); zigbee_devices.setTimer(srcaddr, 0 /* groupaddr */, USE_ZIGBEE_COALESCE_ATTR_TIMER, clusterid, srcendpoint, Z_CAT_READ_ATTR, 0, &Z_PublishAttributes); } else { // Publish immediately - zigbee_devices.jsonPublishNow(srcaddr, json); - - // Add auto-responder here - Z_AutoResponder(srcaddr, clusterid, srcendpoint, json[F("ReadNames")]); + zigbee_devices.jsonPublishNow(srcaddr, attr_list); } } } @@ -1281,30 +1269,8 @@ int32_t EZ_Recv_Default(int32_t res, const class SBuffer &buf) { * Callbacks \*********************************************************************************************/ - -// Aqara Occupancy behavior: the Aqara device only sends Occupancy: true events every 60 seconds. -// Here we add a timer so if we don't receive a Occupancy event for 90 seconds, we send Occupancy:false -void Z_AqaraOccupancy(uint16_t shortaddr, uint16_t cluster, uint8_t endpoint, const JsonObject &json) { - static const uint32_t OCCUPANCY_TIMEOUT = 90 * 1000; // 90 s - // Read OCCUPANCY value if any - const JsonVariant &val_endpoint = GetCaseInsensitive(json, PSTR(OCCUPANCY)); - if (nullptr != &val_endpoint) { - uint32_t occupancy = strToUInt(val_endpoint); - - if (occupancy) { - zigbee_devices.setTimer(shortaddr, 0 /* groupaddr */, OCCUPANCY_TIMEOUT, cluster, endpoint, Z_CAT_VIRTUAL_OCCUPANCY, 0, &Z_OccupancyCallback); - } else { - zigbee_devices.resetTimersForDevice(shortaddr, 0 /* groupaddr */, Z_CAT_VIRTUAL_OCCUPANCY); - } - } -} - - // Publish the received values once they have been coalesced int32_t Z_PublishAttributes(uint16_t shortaddr, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint32_t value) { - const JsonObject *json = zigbee_devices.jsonGet(shortaddr); - if (json == nullptr) { return 0; } // don't crash if not found - zigbee_devices.jsonPublishFlush(shortaddr); return 1; } @@ -1476,49 +1442,61 @@ int32_t Z_State_Ready(uint8_t value) { // // Mostly used for routers/end-devices // json: holds the attributes in JSON format -void Z_AutoResponder(uint16_t srcaddr, uint16_t cluster, uint8_t endpoint, const JsonObject &json) { +void Z_AutoResponder(uint16_t srcaddr, uint16_t cluster, uint8_t endpoint, const uint16_t *attr_list, size_t attr_len) { DynamicJsonBuffer jsonBuffer; JsonObject& json_out = jsonBuffer.createObject(); - // responder - switch (cluster) { - case 0x0000: - if (HasKeyCaseInsensitive(json, PSTR("ModelId"))) { json_out[F("ModelId")] = F(USE_ZIGBEE_MODELID); } - if (HasKeyCaseInsensitive(json, PSTR("Manufacturer"))) { json_out[F("Manufacturer")] = F(USE_ZIGBEE_MANUFACTURER); } - break; + for (uint32_t i=0; i 0xFEFF) ? uxy[i] : 0xFEFF; - } - if (HasKeyCaseInsensitive(json, PSTR("Hue"))) { json_out[F("Hue")] = changeUIntScale(hue, 0, 360, 0, 254); } - if (HasKeyCaseInsensitive(json, PSTR("Sat"))) { json_out[F("Sat")] = changeUIntScale(sat, 0, 255, 0, 254); } - if (HasKeyCaseInsensitive(json, PSTR("CT"))) { json_out[F("CT")] = LightGetColorTemp(); } - if (HasKeyCaseInsensitive(json, PSTR("X"))) { json_out[F("X")] = uxy[0]; } - if (HasKeyCaseInsensitive(json, PSTR("Y"))) { json_out[F("Y")] = uxy[1]; } - } - break; + case 0x00060000: json_out[F("Power")] = Light.power ? 1 : 0; break; // Power + case 0x00080000: json_out[F("Dimmer")] = LightGetDimmer(0); break; // Dimmer + + case 0x03000000: // Hue + case 0x03000001: // Sat + case 0x03000003: // X + case 0x03000004: // Y + case 0x03000007: // CT + { + uint16_t hue; + uint8_t sat; + float XY[2]; + LightGetHSB(&hue, &sat, nullptr); + LightGetXY(&XY[0], &XY[1]); + uint16_t uxy[2]; + for (uint32_t i = 0; i < ARRAY_SIZE(XY); i++) { + uxy[i] = XY[i] * 65536.0f; + uxy[i] = (uxy[i] > 0xFEFF) ? uxy[i] : 0xFEFF; + } + if (0x0000 == attr) { json_out[F("Hue")] = changeUIntScale(hue, 0, 360, 0, 254); } + if (0x0001 == attr) { json_out[F("Sat")] = changeUIntScale(sat, 0, 255, 0, 254); } + if (0x0003 == attr) { json_out[F("X")] = uxy[0]; } + if (0x0004 == attr) { json_out[F("Y")] = uxy[1]; } + if (0x0007 == attr) { json_out[F("CT")] = LightGetColorTemp(); } + } + break; #endif - case 0x000A: // Time - if (HasKeyCaseInsensitive(json, PSTR("Time"))) { json_out[F("Time")] = (Rtc.utc_time > (60 * 60 * 24 * 365 * 10)) ? Rtc.utc_time - 946684800 : Rtc.utc_time; } - if (HasKeyCaseInsensitive(json, PSTR("TimeEpoch"))) { json_out[F("TimeEpoch")] = Rtc.utc_time; } - if (HasKeyCaseInsensitive(json, PSTR("TimeStatus"))) { json_out[F("TimeStatus")] = (Rtc.utc_time > (60 * 60 * 24 * 365 * 10)) ? 0x02 : 0x00; } // if time is beyond 2010 then we are synchronized - if (HasKeyCaseInsensitive(json, PSTR("TimeZone"))) { json_out[F("TimeZone")] = Settings.toffset[0] * 60; } // seconds - break; + case 0x000A0000: // Time + json_out[F("Time")] = (Rtc.utc_time > (60 * 60 * 24 * 365 * 10)) ? Rtc.utc_time - 946684800 : Rtc.utc_time; + break; + case 0x000AFF00: // TimeEpoch - Tasmota specific + json_out[F("TimeEpoch")] = Rtc.utc_time; + break; + case 0x000A0001: // TimeStatus + json_out[F("TimeStatus")] = (Rtc.utc_time > (60 * 60 * 24 * 365 * 10)) ? 0x02 : 0x00; // if time is beyond 2010 then we are synchronized + break; + case 0x000A0002: // TimeZone + json_out[F("TimeZone")] = Settings.toffset[0] * 60; + break; + case 0x000A0007: // LocalTime // TODO take DST + json_out[F("LocalTime")] = Settings.toffset[0] * 60 + ((Rtc.utc_time > (60 * 60 * 24 * 365 * 10)) ? Rtc.utc_time - 946684800 : Rtc.utc_time); + break; + } } if (json_out.size() > 0) { diff --git a/tasmota/xdrv_23_zigbee_A_impl.ino b/tasmota/xdrv_23_zigbee_A_impl.ino index a6386e491..4dd7e99ad 100644 --- a/tasmota/xdrv_23_zigbee_A_impl.ino +++ b/tasmota/xdrv_23_zigbee_A_impl.ino @@ -795,7 +795,7 @@ void ZbBindUnbind(bool unbind) { // false = bind, true = unbind if (nullptr != &val_cluster) { cluster = strToUInt(val_cluster); // first convert as number if (0 == cluster) { - zigbeeFindAttributeByName(val_cluster.as(), &cluster, nullptr, nullptr, nullptr); + zigbeeFindAttributeByName(val_cluster.as(), &cluster, nullptr, nullptr); } }