diff --git a/tasmota/CHANGELOG.md b/tasmota/CHANGELOG.md index 487c94e64..9fb3490fc 100644 --- a/tasmota/CHANGELOG.md +++ b/tasmota/CHANGELOG.md @@ -11,6 +11,7 @@ - Add Three Phase Export Active Energy to SDM630 driver - Add wildcard pattern ``?`` for JSON matching in rules - Add support for unique MQTTClient (and inherited fallback topic) by full Mac address using ``mqttclient DVES_%12X`` (#8300) +- Add Zigbee options to ``ZbSend`` to write and report attributes ### 8.3.1.1 20200518 diff --git a/tasmota/i18n.h b/tasmota/i18n.h index 89d931908..e5f3e395a 100644 --- a/tasmota/i18n.h +++ b/tasmota/i18n.h @@ -513,10 +513,15 @@ #define D_CMND_ZIGBEE_FORGET "Forget" #define D_CMND_ZIGBEE_SAVE "Save" #define D_CMND_ZIGBEE_LINKQUALITY "LinkQuality" + #define D_CMND_ZIGBEE_CLUSTER "Cluster" #define D_CMND_ZIGBEE_ENDPOINT "Endpoint" #define D_CMND_ZIGBEE_GROUP "Group" + #define D_CMND_ZIGBEE_MANUF "Manuf" + #define D_CMND_ZIGBEE_DEVICE "Device" #define D_CMND_ZIGBEE_READ "Read" #define D_CMND_ZIGBEE_SEND "Send" +#define D_CMND_ZIGBEE_WRITE "Write" +#define D_CMND_ZIGBEE_REPORT "Report" #define D_JSON_ZIGBEE_ZCL_SENT "ZbZCLSent" #define D_JSON_ZIGBEE_RECEIVED "ZbReceived" #define D_CMND_ZIGBEE_BIND "Bind" diff --git a/tasmota/xdrv_10_rules.ino b/tasmota/xdrv_10_rules.ino index 1ad618485..8dd185a29 100644 --- a/tasmota/xdrv_10_rules.ino +++ b/tasmota/xdrv_10_rules.ino @@ -458,6 +458,21 @@ bool RulesRuleMatch(uint8_t rule_set, String &event, String &rule) rule_param = String(SunMinutes(1)); } #endif // USE_TIMERS and USE_SUNRISE +// #ifdef USE_ZIGBEE +// if (rule_param.startsWith(F("%ZBDEVICE%"))) { +// snprintf_P(stemp, sizeof(stemp), PSTR("0x%04X"), Z_GetLastDevice()); +// rule_param = String(stemp); +// } +// if (rule_param.startsWith(F("%ZBGROUP%"))) { +// rule_param = String(Z_GetLastGroup()); +// } +// if (rule_param.startsWith(F("%ZBCLUSTER%"))) { +// rule_param = String(Z_GetLastCluster()); +// } +// if (rule_param.startsWith(F("%ZBENDPOINT%"))) { +// rule_param = String(Z_GetLastEndpoint()); +// } +// #endif rule_param.toUpperCase(); strlcpy(rule_svalue, rule_param.c_str(), sizeof(rule_svalue)); @@ -701,6 +716,13 @@ bool RuleSetProcess(uint8_t rule_set, String &event_saved) RulesVarReplace(commands, F("%SUNRISE%"), String(SunMinutes(0))); RulesVarReplace(commands, F("%SUNSET%"), String(SunMinutes(1))); #endif // USE_TIMERS and USE_SUNRISE +#ifdef USE_ZIGBEE + snprintf_P(stemp, sizeof(stemp), PSTR("0x%04X"), Z_GetLastDevice()); + RulesVarReplace(commands, F("%ZBDEVICE%"), String(stemp)); + RulesVarReplace(commands, F("%ZBGROUP%"), String(Z_GetLastGroup())); + RulesVarReplace(commands, F("%ZBCLUSTER%"), String(Z_GetLastCluster())); + RulesVarReplace(commands, F("%ZBENDPOINT%"), String(Z_GetLastEndpoint())); +#endif char command[commands.length() +1]; strlcpy(command, commands.c_str(), sizeof(command)); @@ -1261,6 +1283,16 @@ bool findNextVariableValue(char * &pVarname, float &value) } else if (sVarName.equals(F("SUNSET"))) { value = SunMinutes(1); #endif +// #ifdef USE_ZIGBEE +// // } else if (sVarName.equals(F("ZBDEVICE"))) { +// // value = Z_GetLastDevice(); +// } else if (sVarName.equals(F("ZBGROUP"))) { +// value = Z_GetLastGroup(); +// } else if (sVarName.equals(F("ZBCLUSTER"))) { +// value = Z_GetLastCluster(); +// } else if (sVarName.equals(F("ZBENDPOINT"))) { +// value = Z_GetLastEndpoint(); +// #endif } else { succeed = false; } diff --git a/tasmota/xdrv_23_zigbee_2_devices.ino b/tasmota/xdrv_23_zigbee_2_devices.ino index c115052f7..60f116002 100644 --- a/tasmota/xdrv_23_zigbee_2_devices.ino +++ b/tasmota/xdrv_23_zigbee_2_devices.ino @@ -26,6 +26,24 @@ #endif const uint16_t kZigbeeSaveDelaySeconds = ZIGBEE_SAVE_DELAY_SECONDS; // wait for x seconds +/*********************************************************************************************\ + * 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 \*********************************************************************************************/ @@ -256,7 +274,7 @@ int32_t Z_Devices::findEndpointInVector(const std::vector & vecOfElements, u // entry with same shortaddr or longaddr exists. // Z_Device & Z_Devices::createDeviceEntry(uint16_t shortaddr, uint64_t longaddr) { - if (!shortaddr && !longaddr) { return *(Z_Device*) nullptr; } // it is not legal to create an enrty with both short/long addr null + if ((BAD_SHORTADDR == shortaddr) && !longaddr) { return *(Z_Device*) nullptr; } // it is not legal to create this entry //Z_Device* device_alloc = (Z_Device*) malloc(sizeof(Z_Device)); Z_Device* device_alloc = new Z_Device{ longaddr, @@ -340,7 +358,7 @@ int32_t Z_Devices::findFriendlyName(const char * name) const { if (name_len) { for (auto &elem : _devices) { if (elem->friendlyName) { - if (strcmp(elem->friendlyName, name) == 0) { return found; } + if (strcasecmp(elem->friendlyName, name) == 0) { return found; } } found++; } @@ -860,21 +878,21 @@ const JsonObject *Z_Devices::jsonGet(uint16_t shortaddr) { void Z_Devices::jsonPublishFlush(uint16_t shortaddr) { Z_Device & device = getShortAddr(shortaddr); if (&device == nullptr) { return; } // don't crash if not found - JsonObject * json = device.json; - if (json == nullptr) { return; } // abort if nothing in buffer + JsonObject & json = *device.json; + if (&json == nullptr) { return; } // abort if nothing in buffer const char * fname = zigbee_devices.getFriendlyName(shortaddr); bool use_fname = (Settings.flag4.zigbee_use_names) && (fname); // should we replace shortaddr with friendlyname? - // Remove redundant "Name" or "Device" - if (use_fname) { - json->remove(F(D_JSON_ZIGBEE_NAME)); - } else { - json->remove(F(D_JSON_ZIGBEE_DEVICE)); - } + // 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% + // dump json in string String msg = ""; - json->printTo(msg); + json.printTo(msg); zigbee_devices.jsonClear(shortaddr); if (use_fname) { @@ -889,7 +907,7 @@ void Z_Devices::jsonPublishFlush(uint16_t shortaddr) { } else { MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain); } - XdrvRulesProcess(); + XdrvRulesProcess(); // apply rules } void Z_Devices::jsonPublishNow(uint16_t shortaddr, JsonObject & values) { @@ -923,7 +941,7 @@ uint16_t Z_Devices::parseDeviceParam(const char * param, bool short_must_be_know if ((XdrvMailbox.payload > 0) && (XdrvMailbox.payload <= 99)) { shortaddr = zigbee_devices.isKnownIndex(XdrvMailbox.payload - 1); } - } else if ((dataBuf[0] == '0') && (dataBuf[1] == 'x')) { + } else if ((dataBuf[0] == '0') && ((dataBuf[1] == 'x') || (dataBuf[1] == 'X'))) { // starts with 0x if (strlen(dataBuf) < 18) { // expect a short address diff --git a/tasmota/xdrv_23_zigbee_5_converters.ino b/tasmota/xdrv_23_zigbee_5_converters.ino index dfb69d4a7..5da74a1ed 100644 --- a/tasmota/xdrv_23_zigbee_5_converters.ino +++ b/tasmota/xdrv_23_zigbee_5_converters.ino @@ -79,476 +79,6 @@ uint8_t Z_getDatatypeLen(uint8_t t) { } } -typedef union ZCLHeaderFrameControl_t { - struct { - uint8_t frame_type : 2; // 00 = across entire profile, 01 = cluster specific - uint8_t manuf_specific : 1; // Manufacturer Specific Sub-field - uint8_t direction : 1; // 0 = tasmota to zigbee, 1 = zigbee to tasmota - uint8_t disable_def_resp : 1; // don't send back default response - uint8_t reserved : 3; - } b; - uint32_t d8; // raw 8 bits field -} ZCLHeaderFrameControl_t; - - -class ZCLFrame { -public: - - ZCLFrame(uint8_t frame_control, uint16_t manuf_code, uint8_t transact_seq, uint8_t cmd_id, - const char *buf, size_t buf_len, uint16_t clusterid, uint16_t groupaddr, - uint16_t srcaddr, uint8_t srcendpoint, uint8_t dstendpoint, uint8_t wasbroadcast, - uint8_t linkquality, uint8_t securityuse, uint8_t seqnumber, - uint32_t timestamp): - _manuf_code(manuf_code), _transact_seq(transact_seq), _cmd_id(cmd_id), - _payload(buf_len ? buf_len : 250), // allocate the data frame from source or preallocate big enough - _cluster_id(clusterid), _groupaddr(groupaddr), - _srcaddr(srcaddr), _srcendpoint(srcendpoint), _dstendpoint(dstendpoint), _wasbroadcast(wasbroadcast), - _linkquality(linkquality), _securityuse(securityuse), _seqnumber(seqnumber), - _timestamp(timestamp) - { - _frame_control.d8 = frame_control; - _payload.addBuffer(buf, buf_len); - }; - - - void log(void) { - char hex_char[_payload.len()*2+2]; - ToHex_P((unsigned char*)_payload.getBuffer(), _payload.len(), hex_char, sizeof(hex_char)); - Response_P(PSTR("{\"" D_JSON_ZIGBEEZCL_RECEIVED "\":{" - "\"groupid\":%d," "\"clusterid\":%d," "\"srcaddr\":\"0x%04X\"," - "\"srcendpoint\":%d," "\"dstendpoint\":%d," "\"wasbroadcast\":%d," - "\"" D_CMND_ZIGBEE_LINKQUALITY "\":%d," "\"securityuse\":%d," "\"seqnumber\":%d," - "\"timestamp\":%d," - "\"fc\":\"0x%02X\",\"manuf\":\"0x%04X\",\"transact\":%d," - "\"cmdid\":\"0x%02X\",\"payload\":\"%s\"}}"), - _groupaddr, _cluster_id, _srcaddr, - _srcendpoint, _dstendpoint, _wasbroadcast, - _linkquality, _securityuse, _seqnumber, - _timestamp, - _frame_control, _manuf_code, _transact_seq, _cmd_id, - hex_char); - if (Settings.flag3.tuya_serial_mqtt_publish) { - MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR)); - XdrvRulesProcess(); - } else { - AddLog_P2(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "%s"), mqtt_data); - } - } - - static ZCLFrame parseRawFrame(const SBuffer &buf, uint8_t offset, uint8_t len, uint16_t clusterid, uint16_t groupid, - uint16_t srcaddr, uint8_t srcendpoint, uint8_t dstendpoint, uint8_t wasbroadcast, - uint8_t linkquality, uint8_t securityuse, uint8_t seqnumber, - uint32_t timestamp) { // parse a raw frame and build the ZCL frame object - uint32_t i = offset; - ZCLHeaderFrameControl_t frame_control; - uint16_t manuf_code = 0; - uint8_t transact_seq; - uint8_t cmd_id; - - frame_control.d8 = buf.get8(i++); - if (frame_control.b.manuf_specific) { - manuf_code = buf.get16(i); - i += 2; - } - transact_seq = buf.get8(i++); - cmd_id = buf.get8(i++); - ZCLFrame zcl_frame(frame_control.d8, manuf_code, transact_seq, cmd_id, - (const char *)(buf.buf() + i), len + offset - i, - clusterid, groupid, - srcaddr, srcendpoint, dstendpoint, wasbroadcast, - linkquality, securityuse, seqnumber, - timestamp); - return zcl_frame; - } - - bool isClusterSpecificCommand(void) { - 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 parseRawAttributes(JsonObject& json, uint8_t offset = 0); - void parseReadAttributes(JsonObject& json, uint8_t offset = 0); - void parseResponse(void); - void parseClusterSpecificCommand(JsonObject& json, uint8_t offset = 0); - void postProcessAttributes(uint16_t shortaddr, JsonObject& json); - - inline void setGroupId(uint16_t groupid) { - _groupaddr = groupid; - } - - inline void setClusterId(uint16_t clusterid) { - _cluster_id = clusterid; - } - - inline uint8_t getCmdId(void) const { - return _cmd_id; - } - - inline uint16_t getClusterId(void) const { - return _cluster_id; - } - - inline uint16_t getSrcEndpoint(void) const { - return _srcendpoint; - } - - const SBuffer &getPayload(void) const { - return _payload; - } - - uint16_t getManufCode(void) const { - return _manuf_code; - } - -private: - ZCLHeaderFrameControl_t _frame_control = { .d8 = 0 }; - uint16_t _manuf_code = 0; // optional - uint8_t _transact_seq = 0; // transaction sequence number - uint8_t _cmd_id = 0; - SBuffer _payload; - uint16_t _cluster_id = 0; - uint16_t _groupaddr = 0; - // information from decoded ZCL frame - uint16_t _srcaddr; - uint8_t _srcendpoint; - uint8_t _dstendpoint; - uint8_t _wasbroadcast; - uint8_t _linkquality; - uint8_t _securityuse; - uint8_t _seqnumber; - uint32_t _timestamp; -}; - -// Zigbee ZCL converters - -// from https://github.com/Koenkk/zigbee-shepherd-converters/blob/638d29f0cace6343052b9a4e7fd60980fa785479/converters/fromZigbee.js#L55 -// Input voltage in mV, i.e. 3000 = 3.000V -// Output percentage from 0 to 100 as int -uint8_t toPercentageCR2032(uint32_t voltage) { - uint32_t percentage; - if (voltage < 2100) { - percentage = 0; - } else if (voltage < 2440) { - percentage = 6 - ((2440 - voltage) * 6) / 340; - } else if (voltage < 2740) { - percentage = 18 - ((2740 - voltage) * 12) / 300; - } else if (voltage < 2900) { - percentage = 42 - ((2900 - voltage) * 24) / 160; - } else if (voltage < 3000) { - percentage = 100 - ((3000 - voltage) * 58) / 100; - } else if (voltage >= 3000) { - percentage = 100; - } - return percentage; -} - - -uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer &buf, - uint32_t offset, uint32_t buflen) { - - uint32_t i = offset; - uint32_t attrtype = buf.get8(i++); - - // fallback - enter a null value - json[attrid_str] = (char*) nullptr; - - uint32_t len = Z_getDatatypeLen(attrtype); // pre-compute lenght, overloaded for variable length attributes - - // now parse accordingly to attr type - switch (attrtype) { - // case Znodata: // nodata - // case Zunk: // unk - // break; - case Zbool: // bool - case Zuint8: // uint8 - case Zenum8: // enum8 - { - uint8_t uint8_val = buf.get8(i); - // i += 1; - if (0xFF != uint8_val) { - json[attrid_str] = uint8_val; - } - } - break; - case Zuint16: // uint16 - case Zenum16: // enum16 - { - uint16_t uint16_val = buf.get16(i); - // i += 2; - if (0xFFFF != uint16_val) { - json[attrid_str] = uint16_val; - } - } - break; - case Zuint32: // uint32 - { - uint32_t uint32_val = buf.get32(i); - // i += 4; - if (0xFFFFFFFF != uint32_val) { - json[attrid_str] = uint32_val; - } - } - break; - // Note: uint40, uint48, uint56, uint64 are displayed as Hex - // Note: int40, int48, int56, int64 are displayed as Hex - case Zuint40: // uint40 - case Zuint48: // uint48 - case Zuint56: // uint56 - case Zuint64: // uint64 - case Zint40: // int40 - case Zint48: // int48 - case Zint56: // int56 - case Zint64: // int64 - { - // uint8_t len = attrtype - 0x27; // 5 - 8 - // print as HEX - char hex[2*len+1]; - ToHex_P(buf.buf(i), len, hex, sizeof(hex)); - json[attrid_str] = hex; - // i += len; - } - break; - case Zint8: // int8 - { - int8_t int8_val = buf.get8(i); - // i += 1; - if (0x80 != int8_val) { - json[attrid_str] = int8_val; - } - } - break; - case Zint16: // int16 - { - int16_t int16_val = buf.get16(i); - // i += 2; - if (0x8000 != int16_val) { - json[attrid_str] = int16_val; - } - } - break; - case Zint32: // int32 - { - int32_t int32_val = buf.get32(i); - // i += 4; - if (0x80000000 != int32_val) { - json[attrid_str] = int32_val; - } - } - break; - - case Zoctstr: // octet string, 1 byte len - case Zstring: // char string, 1 byte len - case Zoctstr16: // octet string, 2 bytes len - case Zstring16: // char string, 2 bytes len - // For strings, default is to try to do a real string, but reverts to octet stream if null char is present or on some exceptions - { - bool parse_as_string = true; - len = (attrtype <= 0x42) ? buf.get8(i) : buf.get16(i); // len is 8 or 16 bits - i += (attrtype <= 0x42) ? 1 : 2; // increment pointer - if (i + len > buf.len()) { // make sure we don't get past the buffer - len = buf.len() - i; - } - - // check if we can safely use a string - if ((0x41 == attrtype) || (0x43 == attrtype)) { parse_as_string = false; } - - if (parse_as_string) { - char str[len+1]; - strncpy(str, buf.charptr(i), len); - str[len] = 0x00; - json[attrid_str] = str; - } else { - // print as HEX - char hex[2*len+1]; - ToHex_P(buf.buf(i), len, hex, sizeof(hex)); - json[attrid_str] = hex; - } - - // i += len; - // break; - } - // i += buf.get8(i) + 1; - break; - - case Zdata8: // data8 - case Zmap8: // map8 - { - uint8_t uint8_val = buf.get8(i); - // i += 1; - json[attrid_str] = uint8_val; - } - break; - case Zdata16: // data16 - case Zmap16: // map16 - { - uint16_t uint16_val = buf.get16(i); - // i += 2; - json[attrid_str] = uint16_val; - } - break; - case Zdata32: // data32 - case Zmap32: // map32 - { - uint32_t uint32_val = buf.get32(i); - // i += 4; - json[attrid_str] = uint32_val; - } - break; - - case Zsingle: // float - { - uint32_t uint32_val = buf.get32(i); - float * float_val = (float*) &uint32_val; - // i += 4; - json[attrid_str] = *float_val; - } - break; - - // TODO - case ZToD: // ToD - case Zdate: // date - case ZUTC: // UTC - case ZclusterId: // clusterId - case ZattribId: // attribId - case ZbacOID: // bacOID - case ZEUI64: // EUI64 - case Zkey128: // key128 - case Zsemi: // semi (float on 2 bytes) - break; - - // Other un-implemented data types - case Zdata24: // data24 - case Zdata40: // data40 - case Zdata48: // data48 - case Zdata56: // data56 - case Zdata64: // data64 - break; - // map - case Zmap24: // map24 - case Zmap40: // map40 - case Zmap48: // map48 - case Zmap56: // map56 - case Zmap64: // map64 - break; - case Zdouble: // double precision - { - uint64_t uint64_val = buf.get64(i); - double * double_val = (double*) &uint64_val; - // i += 8; - json[attrid_str] = *double_val; - } - break; - } - i += len; - - // String pp; // pretty print - // json[attrid_str].prettyPrintTo(pp); - // // now store the attribute - // AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "ZCL attribute decoded, id %s, type 0x%02X, val=%s"), - // attrid_str, attrtype, pp.c_str()); - return i - offset; // how much have we increased the index -} - -// Generate an attribute name based on cluster number, attribute, and suffix if duplicates -void ZCLFrame::generateAttributeName(const JsonObject& json, uint16_t cluster, uint16_t attr, char *key, size_t key_len) { - uint32_t suffix = 1; - - snprintf_P(key, key_len, PSTR("%04X/%04X"), cluster, attr); - while (json.containsKey(key)) { - suffix++; - snprintf_P(key, key_len, PSTR("%04X/%04X+%d"), cluster, attr, suffix); // add "0008/0001+2" suffix if duplicate - } -} - -// First pass, parse all attributes in their native format -void ZCLFrame::parseRawAttributes(JsonObject& json, uint8_t offset) { - uint32_t i = offset; - uint32_t len = _payload.len(); - - while (len >= 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, len); - } -} - -// ZCL_READ_ATTRIBUTES_RESPONSE -void ZCLFrame::parseReadAttributes(JsonObject& json, uint8_t offset) { - uint32_t i = offset; - uint32_t len = _payload.len(); - - while (len - i >= 4) { - uint16_t attrid = _payload.get16(i); - i += 2; - 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, len); - } - } -} - -// ZCL_DEFAULT_RESPONSE -void ZCLFrame::parseResponse(void) { - if (_payload.len() < 2) { return; } // wrong format - uint8_t cmd = _payload.get8(0); - uint8_t status = _payload.get8(1); - - DynamicJsonBuffer jsonBuffer; - JsonObject& json = jsonBuffer.createObject(); - - // "Device" - char s[12]; - snprintf_P(s, sizeof(s), PSTR("0x%04X"), _srcaddr); - json[F(D_JSON_ZIGBEE_DEVICE)] = s; - // "Name" - const char * friendlyName = zigbee_devices.getFriendlyName(_srcaddr); - if (friendlyName) { - json[F(D_JSON_ZIGBEE_NAME)] = (char*) friendlyName; - } - // "Command" - snprintf_P(s, sizeof(s), PSTR("%04X!%02X"), _cluster_id, cmd); - json[F(D_JSON_ZIGBEE_CMD)] = s; - // "Status" - json[F(D_JSON_ZIGBEE_STATUS)] = status; - // "StatusMessage" - json[F(D_JSON_ZIGBEE_STATUS_MSG)] = getZigbeeStatusMessage(status); - // Add Endpoint - json[F(D_CMND_ZIGBEE_ENDPOINT)] = _srcendpoint; - // Add Group if non-zero - if (_groupaddr) { - json[F(D_CMND_ZIGBEE_GROUP)] = _groupaddr; - } - // Add linkquality - json[F(D_CMND_ZIGBEE_LINKQUALITY)] = _linkquality; - - String msg(""); - msg.reserve(100); - json.printTo(msg); - Response_P(PSTR("{\"" D_JSON_ZIGBEE_RESPONSE "\":%s}"), msg.c_str()); - MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_ZIGBEEZCL_RECEIVED)); - XdrvRulesProcess(); -} - - -// Parse non-normalized attributes -void ZCLFrame::parseClusterSpecificCommand(JsonObject& json, uint8_t offset) { - convertClusterSpecific(json, _cluster_id, _cmd_id, _frame_control.b.direction, _payload); - sendHueUpdate(_srcaddr, _groupaddr, _cluster_id, _cmd_id, _frame_control.b.direction); -} // return value: // 0 = keep initial value @@ -684,13 +214,13 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { { Zuint8, Cx0000, 0x0002, Z(StackVersion), &Z_Copy }, { Zuint8, Cx0000, 0x0003, Z(HWVersion), &Z_Copy }, { Zstring, Cx0000, 0x0004, Z(Manufacturer), &Z_ManufKeep }, // record Manufacturer - { Zstring, Cx0000, 0x0005, Z(ModelId), &Z_ModelKeep }, // record Model + { Zstring, Cx0000, 0x0005, Z(ModelId), &Z_ModelKeep }, // record Model { Zstring, Cx0000, 0x0006, Z(DateCode), &Z_Copy }, { Zenum8, Cx0000, 0x0007, Z(PowerSource), &Z_Copy }, { Zstring, Cx0000, 0x4000, Z(SWBuildID), &Z_Copy }, - { Zunk, Cx0000, 0xFFFF, nullptr, &Z_Remove }, // Remove all other values + { Zunk, Cx0000, 0xFFFF, nullptr, &Z_Remove }, // Remove all other values // Cmd 0x0A - Cluster 0x0000, attribute 0xFF01 - proprietary - { Zmap8, Cx0000, 0xFF01, nullptr, &Z_AqaraSensor }, // Occupancy (map8) + { Zmap8, Cx0000, 0xFF01, nullptr, &Z_AqaraSensor }, // Occupancy (map8) // Power Configuration cluster { Zuint16, Cx0001, 0x0000, Z(MainsVoltage), &Z_Copy }, @@ -712,8 +242,8 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { //{ Zmap8, Cx0005, 0x0004, Z(NameSupport), &Z_Copy }, // On/off cluster - { Zbool, Cx0006, 0x0000, Z(Power), &Z_Copy }, - { Zbool, Cx0006, 0x8000, Z(Power), &Z_Copy }, // See 7280 + { Zbool, Cx0006, 0x0000, Z(Power), &Z_Copy }, + { Zbool, Cx0006, 0x8000, Z(Power), &Z_Copy }, // See 7280 // On/Off Switch Configuration cluster { Zenum8, Cx0007, 0x0000, Z(SwitchType), &Z_Copy }, @@ -735,7 +265,7 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { { Zmap8, Cx000A, 0x0001, Z(TimeStatus), &Z_Copy }, { Zint32, Cx000A, 0x0002, Z(TimeZone), &Z_Copy }, { Zuint32, Cx000A, 0x0003, Z(DstStart), &Z_Copy }, - { Zuint32, Cx000A, 0x0004, Z(DstEnd), &Z_Copy }, + { Zuint32, Cx000A, 0x0004, Z(DstEnd), &Z_Copy }, { Zint32, Cx000A, 0x0005, Z(DstShift), &Z_Copy }, { Zuint32, Cx000A, 0x0006, Z(StandardTime), &Z_Copy }, { Zuint32, Cx000A, 0x0007, Z(LocalTime), &Z_Copy }, @@ -1032,6 +562,590 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { }; + +typedef union ZCLHeaderFrameControl_t { + struct { + uint8_t frame_type : 2; // 00 = across entire profile, 01 = cluster specific + uint8_t manuf_specific : 1; // Manufacturer Specific Sub-field + uint8_t direction : 1; // 0 = tasmota to zigbee, 1 = zigbee to tasmota + uint8_t disable_def_resp : 1; // don't send back default response + uint8_t reserved : 3; + } b; + uint32_t d8; // raw 8 bits field +} ZCLHeaderFrameControl_t; + + +class ZCLFrame { +public: + + ZCLFrame(uint8_t frame_control, uint16_t manuf_code, uint8_t transact_seq, uint8_t cmd_id, + const char *buf, size_t buf_len, uint16_t clusterid, uint16_t groupaddr, + uint16_t srcaddr, uint8_t srcendpoint, uint8_t dstendpoint, uint8_t wasbroadcast, + uint8_t linkquality, uint8_t securityuse, uint8_t seqnumber, + uint32_t timestamp): + _manuf_code(manuf_code), _transact_seq(transact_seq), _cmd_id(cmd_id), + _payload(buf_len ? buf_len : 250), // allocate the data frame from source or preallocate big enough + _cluster_id(clusterid), _groupaddr(groupaddr), + _srcaddr(srcaddr), _srcendpoint(srcendpoint), _dstendpoint(dstendpoint), _wasbroadcast(wasbroadcast), + _linkquality(linkquality), _securityuse(securityuse), _seqnumber(seqnumber), + _timestamp(timestamp) + { + _frame_control.d8 = frame_control; + _payload.addBuffer(buf, buf_len); + }; + + + void log(void) { + char hex_char[_payload.len()*2+2]; + ToHex_P((unsigned char*)_payload.getBuffer(), _payload.len(), hex_char, sizeof(hex_char)); + Response_P(PSTR("{\"" D_JSON_ZIGBEEZCL_RECEIVED "\":{" + "\"groupid\":%d," "\"clusterid\":%d," "\"srcaddr\":\"0x%04X\"," + "\"srcendpoint\":%d," "\"dstendpoint\":%d," "\"wasbroadcast\":%d," + "\"" D_CMND_ZIGBEE_LINKQUALITY "\":%d," "\"securityuse\":%d," "\"seqnumber\":%d," + "\"timestamp\":%d," + "\"fc\":\"0x%02X\",\"manuf\":\"0x%04X\",\"transact\":%d," + "\"cmdid\":\"0x%02X\",\"payload\":\"%s\"}}"), + _groupaddr, _cluster_id, _srcaddr, + _srcendpoint, _dstendpoint, _wasbroadcast, + _linkquality, _securityuse, _seqnumber, + _timestamp, + _frame_control, _manuf_code, _transact_seq, _cmd_id, + hex_char); + if (Settings.flag3.tuya_serial_mqtt_publish) { + MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR)); + XdrvRulesProcess(); + } else { + AddLog_P2(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "%s"), mqtt_data); + } + } + + static ZCLFrame parseRawFrame(const SBuffer &buf, uint8_t offset, uint8_t len, uint16_t clusterid, uint16_t groupid, + uint16_t srcaddr, uint8_t srcendpoint, uint8_t dstendpoint, uint8_t wasbroadcast, + uint8_t linkquality, uint8_t securityuse, uint8_t seqnumber, + uint32_t timestamp) { // parse a raw frame and build the ZCL frame object + uint32_t i = offset; + ZCLHeaderFrameControl_t frame_control; + uint16_t manuf_code = 0; + uint8_t transact_seq; + uint8_t cmd_id; + + frame_control.d8 = buf.get8(i++); + if (frame_control.b.manuf_specific) { + manuf_code = buf.get16(i); + i += 2; + } + transact_seq = buf.get8(i++); + cmd_id = buf.get8(i++); + ZCLFrame zcl_frame(frame_control.d8, manuf_code, transact_seq, cmd_id, + (const char *)(buf.buf() + i), len + offset - i, + clusterid, groupid, + srcaddr, srcendpoint, dstendpoint, wasbroadcast, + linkquality, securityuse, seqnumber, + timestamp); + return zcl_frame; + } + + bool isClusterSpecificCommand(void) { + 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 parseRawAttributes(JsonObject& json, uint8_t offset = 0); + void parseReadAttributes(JsonObject& json, uint8_t offset = 0); + void parseReadAttributesResponse(JsonObject& json, uint8_t offset = 0); + void parseResponse(void); + void parseClusterSpecificCommand(JsonObject& json, uint8_t offset = 0); + void postProcessAttributes(uint16_t shortaddr, JsonObject& json); + + inline void setGroupId(uint16_t groupid) { + _groupaddr = groupid; + } + + inline void setClusterId(uint16_t clusterid) { + _cluster_id = clusterid; + } + + inline uint8_t getCmdId(void) const { + return _cmd_id; + } + + inline uint16_t getClusterId(void) const { + return _cluster_id; + } + + inline uint16_t getSrcEndpoint(void) const { + return _srcendpoint; + } + + const SBuffer &getPayload(void) const { + return _payload; + } + + uint16_t getManufCode(void) const { + return _manuf_code; + } + +private: + ZCLHeaderFrameControl_t _frame_control = { .d8 = 0 }; + uint16_t _manuf_code = 0; // optional + uint8_t _transact_seq = 0; // transaction sequence number + uint8_t _cmd_id = 0; + SBuffer _payload; + uint16_t _cluster_id = 0; + uint16_t _groupaddr = 0; + // information from decoded ZCL frame + uint16_t _srcaddr; + uint8_t _srcendpoint; + uint8_t _dstendpoint; + uint8_t _wasbroadcast; + uint8_t _linkquality; + uint8_t _securityuse; + uint8_t _seqnumber; + uint32_t _timestamp; +}; + +// Zigbee ZCL converters + +// from https://github.com/Koenkk/zigbee-shepherd-converters/blob/638d29f0cace6343052b9a4e7fd60980fa785479/converters/fromZigbee.js#L55 +// Input voltage in mV, i.e. 3000 = 3.000V +// Output percentage from 0 to 100 as int +uint8_t toPercentageCR2032(uint32_t voltage) { + uint32_t percentage; + if (voltage < 2100) { + percentage = 0; + } else if (voltage < 2440) { + percentage = 6 - ((2440 - voltage) * 6) / 340; + } else if (voltage < 2740) { + percentage = 18 - ((2740 - voltage) * 12) / 300; + } else if (voltage < 2900) { + percentage = 42 - ((2900 - voltage) * 24) / 160; + } else if (voltage < 3000) { + percentage = 100 - ((3000 - voltage) * 58) / 100; + } else if (voltage >= 3000) { + percentage = 100; + } + return percentage; +} + +// +// Appends the attribute value to Write or to Report +// Adds to buf: +// - 2 bytes: attribute identigier +// - 1 byte: attribute type +// - n bytes: value (typically between 1 and 4 bytes, or bigger for strings) +// returns number of bytes of attribute, or <0 if error +int32_t encodeSingleAttribute(class SBuffer &buf, const JsonVariant &val, uint16_t attr, uint8_t attrtype) { + uint32_t len = Z_getDatatypeLen(attrtype); // pre-compute lenght, overloaded for variable length attributes + + uint32_t u32 = val.as(); + int32_t i32 = val.as(); + float f32 = val.as(); + + buf.add16(attr); // prepend with attribute identifier + buf.add8(attrtype); // prepend with attribute type + + switch (attrtype) { + // unsigned 8 + case Zbool: // bool + case Zuint8: // uint8 + case Zenum8: // enum8 + case Zdata8: // data8 + case Zmap8: // map8 + buf.add8(u32); + break; + // unsigned 16 + case Zuint16: // uint16 + case Zenum16: // enum16 + case Zdata16: // data16 + case Zmap16: // map16 + buf.add16(u32); + break; + // unisgned 32 + case Zuint32: // uint32 + case Zdata32: // data32 + case Zmap32: // map32 + buf.add32(u32); + break; + + // signed 8 + case Zint8: // int8 + buf.add8(i32); + break; + case Zint16: // int16 + buf.add16(i32); + break; + case Zint32: // int32 + buf.add32(i32); + break; + + case Zsingle: // float + uint32_t *f_ptr; + buf.add32( *((uint32_t*)&f32) ); // cast float as uint32_t + break; + + case Zstring: + case Zstring16: + { + const char * val_str = val.as(); + if (nullptr == val_str) { return -2; } + size_t val_len = strlen(val_str); + if (val_len > 32) { val_len = 32; } + len = val_len + 1; + buf.add8(val_len); + if (Zstring16 == attrtype) { + buf.add8(0); // len is on 2 bytes + len++; + } + for (uint32_t i = 0; i < val_len; i++) { + buf.add8(val_str[i]); + } + } + break; + + default: + // remove the attribute type we just added + buf.setLen(buf.len() - 3); + return -1; + } + return len + 3; +} + +uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer &buf, + uint32_t offset, uint32_t buflen) { + + uint32_t i = offset; + uint32_t attrtype = buf.get8(i++); + + // fallback - enter a null value + json[attrid_str] = (char*) nullptr; + + uint32_t len = Z_getDatatypeLen(attrtype); // pre-compute lenght, overloaded for variable length attributes + + // now parse accordingly to attr type + switch (attrtype) { + // case Znodata: // nodata + // case Zunk: // unk + // break; + case Zbool: // bool + case Zuint8: // uint8 + case Zenum8: // enum8 + { + uint8_t uint8_val = buf.get8(i); + // i += 1; + if (0xFF != uint8_val) { + json[attrid_str] = uint8_val; + } + } + break; + case Zuint16: // uint16 + case Zenum16: // enum16 + { + uint16_t uint16_val = buf.get16(i); + // i += 2; + if (0xFFFF != uint16_val) { + json[attrid_str] = uint16_val; + } + } + break; + case Zuint32: // uint32 + { + uint32_t uint32_val = buf.get32(i); + // i += 4; + if (0xFFFFFFFF != uint32_val) { + json[attrid_str] = uint32_val; + } + } + break; + // Note: uint40, uint48, uint56, uint64 are displayed as Hex + // Note: int40, int48, int56, int64 are displayed as Hex + case Zuint40: // uint40 + case Zuint48: // uint48 + case Zuint56: // uint56 + case Zuint64: // uint64 + case Zint40: // int40 + case Zint48: // int48 + case Zint56: // int56 + case Zint64: // int64 + { + // uint8_t len = attrtype - 0x27; // 5 - 8 + // print as HEX + char hex[2*len+1]; + ToHex_P(buf.buf(i), len, hex, sizeof(hex)); + json[attrid_str] = hex; + // i += len; + } + break; + case Zint8: // int8 + { + int8_t int8_val = buf.get8(i); + // i += 1; + if (0x80 != int8_val) { + json[attrid_str] = int8_val; + } + } + break; + case Zint16: // int16 + { + int16_t int16_val = buf.get16(i); + // i += 2; + if (0x8000 != int16_val) { + json[attrid_str] = int16_val; + } + } + break; + case Zint32: // int32 + { + int32_t int32_val = buf.get32(i); + // i += 4; + if (0x80000000 != int32_val) { + json[attrid_str] = int32_val; + } + } + break; + + case Zoctstr: // octet string, 1 byte len + case Zstring: // char string, 1 byte len + case Zoctstr16: // octet string, 2 bytes len + case Zstring16: // char string, 2 bytes len + // For strings, default is to try to do a real string, but reverts to octet stream if null char is present or on some exceptions + { + bool parse_as_string = true; + len = (attrtype <= 0x42) ? buf.get8(i) : buf.get16(i); // len is 8 or 16 bits + i += (attrtype <= 0x42) ? 1 : 2; // increment pointer + if (i + len > buf.len()) { // make sure we don't get past the buffer + len = buf.len() - i; + } + + // check if we can safely use a string + if ((0x41 == attrtype) || (0x43 == attrtype)) { parse_as_string = false; } + + if (parse_as_string) { + char str[len+1]; + strncpy(str, buf.charptr(i), len); + str[len] = 0x00; + json[attrid_str] = str; + } else { + // print as HEX + char hex[2*len+1]; + ToHex_P(buf.buf(i), len, hex, sizeof(hex)); + json[attrid_str] = hex; + } + + // i += len; + // break; + } + // i += buf.get8(i) + 1; + break; + + case Zdata8: // data8 + case Zmap8: // map8 + { + uint8_t uint8_val = buf.get8(i); + // i += 1; + json[attrid_str] = uint8_val; + } + break; + case Zdata16: // data16 + case Zmap16: // map16 + { + uint16_t uint16_val = buf.get16(i); + // i += 2; + json[attrid_str] = uint16_val; + } + break; + case Zdata32: // data32 + case Zmap32: // map32 + { + uint32_t uint32_val = buf.get32(i); + // i += 4; + json[attrid_str] = uint32_val; + } + break; + + case Zsingle: // float + { + uint32_t uint32_val = buf.get32(i); + float * float_val = (float*) &uint32_val; + // i += 4; + json[attrid_str] = *float_val; + } + break; + + // TODO + case ZToD: // ToD + case Zdate: // date + case ZUTC: // UTC + case ZclusterId: // clusterId + case ZattribId: // attribId + case ZbacOID: // bacOID + case ZEUI64: // EUI64 + case Zkey128: // key128 + case Zsemi: // semi (float on 2 bytes) + break; + + // Other un-implemented data types + case Zdata24: // data24 + case Zdata40: // data40 + case Zdata48: // data48 + case Zdata56: // data56 + case Zdata64: // data64 + break; + // map + case Zmap24: // map24 + case Zmap40: // map40 + case Zmap48: // map48 + case Zmap56: // map56 + case Zmap64: // map64 + break; + case Zdouble: // double precision + { + uint64_t uint64_val = buf.get64(i); + double * double_val = (double*) &uint64_val; + // i += 8; + json[attrid_str] = *double_val; + } + break; + } + i += len; + + // String pp; // pretty print + // json[attrid_str].prettyPrintTo(pp); + // // now store the attribute + // AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "ZCL attribute decoded, id %s, type 0x%02X, val=%s"), + // attrid_str, attrtype, pp.c_str()); + return i - offset; // how much have we increased the index +} + +// Generate an attribute name based on cluster number, attribute, and suffix if duplicates +void ZCLFrame::generateAttributeName(const JsonObject& json, uint16_t cluster, uint16_t attr, char *key, size_t key_len) { + uint32_t suffix = 1; + + snprintf_P(key, key_len, PSTR("%04X/%04X"), cluster, attr); + while (json.containsKey(key)) { + suffix++; + snprintf_P(key, key_len, PSTR("%04X/%04X+%d"), cluster, attr, suffix); // add "0008/0001+2" suffix if duplicate + } +} + +// First pass, parse all attributes in their native format +void ZCLFrame::parseRawAttributes(JsonObject& json, uint8_t offset) { + uint32_t i = offset; + uint32_t len = _payload.len(); + + while (len >= 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, len); + } +} + +// ZCL_READ_ATTRIBUTES +// TODO +void ZCLFrame::parseReadAttributes(JsonObject& json, uint8_t offset) { + uint32_t i = offset; + uint32_t len = _payload.len(); + + json[F(D_CMND_ZIGBEE_CLUSTER)] = _cluster_id; + + JsonArray &attr_list = json.createNestedArray(F("Read")); + JsonObject &attr_names = json.createNestedObject(F("ReadNames")); + while (len - i >= 2) { + uint16_t attrid = _payload.get16(i); + attr_list.add(attrid); + + // find the attribute 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); + + if ((conv_cluster == _cluster_id) && (conv_attribute == attrid)) { + attr_names[(const __FlashStringHelper*) converter->name] = true; + break; + } + } + i += 2; + } +} + +// ZCL_READ_ATTRIBUTES_RESPONSE +void ZCLFrame::parseReadAttributesResponse(JsonObject& json, uint8_t offset) { + uint32_t i = offset; + uint32_t len = _payload.len(); + + while (len >= 4 + i) { + uint16_t attrid = _payload.get16(i); + i += 2; + 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, len); + } + } +} + +// ZCL_DEFAULT_RESPONSE +void ZCLFrame::parseResponse(void) { + if (_payload.len() < 2) { return; } // wrong format + uint8_t cmd = _payload.get8(0); + uint8_t status = _payload.get8(1); + + DynamicJsonBuffer jsonBuffer; + JsonObject& json = jsonBuffer.createObject(); + + // "Device" + char s[12]; + snprintf_P(s, sizeof(s), PSTR("0x%04X"), _srcaddr); + json[F(D_JSON_ZIGBEE_DEVICE)] = s; + // "Name" + const char * friendlyName = zigbee_devices.getFriendlyName(_srcaddr); + if (friendlyName) { + json[F(D_JSON_ZIGBEE_NAME)] = (char*) friendlyName; + } + // "Command" + snprintf_P(s, sizeof(s), PSTR("%04X!%02X"), _cluster_id, cmd); + json[F(D_JSON_ZIGBEE_CMD)] = s; + // "Status" + json[F(D_JSON_ZIGBEE_STATUS)] = status; + // "StatusMessage" + json[F(D_JSON_ZIGBEE_STATUS_MSG)] = getZigbeeStatusMessage(status); + // Add Endpoint + json[F(D_CMND_ZIGBEE_ENDPOINT)] = _srcendpoint; + // Add Group if non-zero + if (_groupaddr) { + json[F(D_CMND_ZIGBEE_GROUP)] = _groupaddr; + } + // Add linkquality + json[F(D_CMND_ZIGBEE_LINKQUALITY)] = _linkquality; + + String msg(""); + msg.reserve(100); + json.printTo(msg); + Response_P(PSTR("{\"" D_JSON_ZIGBEE_RESPONSE "\":%s}"), msg.c_str()); + MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_ZIGBEEZCL_RECEIVED)); + XdrvRulesProcess(); +} + + +// Parse non-normalized attributes +void ZCLFrame::parseClusterSpecificCommand(JsonObject& json, uint8_t offset) { + convertClusterSpecific(json, _cluster_id, _cmd_id, _frame_control.b.direction, _payload); + sendHueUpdate(_srcaddr, _groupaddr, _cluster_id, _cmd_id, _frame_control.b.direction); +} + // ====================================================================== // Record Manuf int32_t Z_ManufKeep(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { diff --git a/tasmota/xdrv_23_zigbee_7_statemachine.ino b/tasmota/xdrv_23_zigbee_7_statemachine.ino index 590c795ac..4297a4375 100644 --- a/tasmota/xdrv_23_zigbee_7_statemachine.ino +++ b/tasmota/xdrv_23_zigbee_7_statemachine.ino @@ -106,10 +106,11 @@ enum Zigbee_StateMachine_Instruction_Set { // Labels used in the State Machine -- internal only const uint8_t ZIGBEE_LABEL_INIT_COORD = 10; // Start ZNP as coordinator const uint8_t ZIGBEE_LABEL_START_COORD = 11; // Start ZNP as coordinator -const uint8_t ZIGBEE_LABEL_INIT_ROUTER = 12; // Start ZNP as router +const uint8_t ZIGBEE_LABEL_INIT_ROUTER = 12; // Init ZNP as router const uint8_t ZIGBEE_LABEL_START_ROUTER = 13; // Start ZNP as router -const uint8_t ZIGBEE_LABEL_INIT_DEVICE = 14; // Start ZNP as end-device -// const uint8_t ZIGBEE_LABEL_START_DEVICE = 15; // Start ZNP as end-device - same as ZIGBEE_LABEL_START_ROUTER +const uint8_t ZIGBEE_LABEL_INIT_DEVICE = 14; // Init ZNP as end-device +const uint8_t ZIGBEE_LABEL_START_DEVICE = 15; // Start ZNP as end-device +const uint8_t ZIGBEE_LABEL_START_ROUTER_DEVICE = 16; // Start common to router and device const uint8_t ZIGBEE_LABEL_FACT_RESET_ROUTER_DEVICE_POST = 19; // common post configuration for router and device const uint8_t ZIGBEE_LABEL_READY = 20; // goto label 20 for main loop const uint8_t ZIGBEE_LABEL_MAIN_LOOP = 21; // main loop @@ -400,7 +401,7 @@ void Z_UpdateConfig(uint8_t zb_channel, uint16_t zb_pan_id, uint64_t zb_ext_pani const char kCheckingDeviceConfiguration[] PROGMEM = D_LOG_ZIGBEE "checking device configuration"; const char kConfiguredCoord[] PROGMEM = "Configured, starting coordinator"; const char kConfiguredRouter[] PROGMEM = "Configured, starting router"; -const char kConfiguredDevice[] PROGMEM = "Configured, starting end-device"; +const char kConfiguredDevice[] PROGMEM = "Configured, starting device"; const char kStarted[] PROGMEM = "Started"; const char kZigbeeStarted[] PROGMEM = D_LOG_ZIGBEE "Zigbee started"; const char kResetting[] PROGMEM = "Resetting configuration"; @@ -426,7 +427,7 @@ static const Zigbee_Instruction zb_prog[] PROGMEM = { ZI_WAIT_RECV_FUNC(2000, ZBR_VERSION, &Z_ReceiveCheckVersion) // Check if version is valid // Dispatching whether coordinator, router or end-device - ZI_CALL(&Z_SwitchDeviceType, 0) // goto ZIGBEE_LABEL_START_ROUTER, ZIGBEE_LABEL_START_DEVICE or continue if coordinator + ZI_CALL(&Z_SwitchDeviceType, 0) // goto ZIGBEE_LABEL_INIT_ROUTER, ZIGBEE_LABEL_INIT_DEVICE or continue if coordinator // ====================================================================== // Start as Zigbee Coordinator @@ -537,8 +538,9 @@ static const Zigbee_Instruction zb_prog[] PROGMEM = { ZI_SEND(ZBS_LOGTYPE) // check the logical type ZI_WAIT_RECV(1000, ZBS_LOGTYPE_ROUTER) // it should be coordinator - ZI_LABEL(ZIGBEE_LABEL_START_ROUTER) // Init as a router + // ZI_LABEL(ZIGBEE_LABEL_START_ROUTER) // Init as a router ZI_MQTT_STATE(ZIGBEE_STATUS_STARTING, kConfiguredRouter) + ZI_LABEL(ZIGBEE_LABEL_START_ROUTER_DEVICE) ZI_ON_ERROR_GOTO(ZIGBEE_LABEL_ABORT) ZI_SEND(ZBS_AF_REGISTER_ALL) // Z_AF register for endpoint 01, profile 0x0104 Home Automation ZI_WAIT_RECV(1000, ZBR_AF_REGISTER) @@ -570,7 +572,7 @@ static const Zigbee_Instruction zb_prog[] PROGMEM = { ZI_SEND(ZBS_WNV_ZNPHC) // Write NV ZNP Has Configured ZI_WAIT_RECV(1000, ZBR_WNV_OK) - ZI_GOTO(ZIGBEE_LABEL_START_ROUTER) + ZI_GOTO(ZIGBEE_LABEL_START_ROUTER_DEVICE) // ====================================================================== // Start as Zigbee Device @@ -583,7 +585,8 @@ static const Zigbee_Instruction zb_prog[] PROGMEM = { ZI_SEND(ZBS_LOGTYPE) // check the logical type ZI_WAIT_RECV(1000, ZBS_LOGTYPE_DEVICE) // it should be coordinator - ZI_GOTO(ZIGBEE_LABEL_START_ROUTER) + ZI_MQTT_STATE(ZIGBEE_STATUS_STARTING, kConfiguredDevice) + ZI_GOTO(ZIGBEE_LABEL_START_ROUTER_DEVICE) ZI_LABEL(ZIGBEE_LABEL_FACT_RESET_DEVICE) // Factory reset for router ZI_MQTT_STATE(ZIGBEE_STATUS_RESET_CONF, kResetting) diff --git a/tasmota/xdrv_23_zigbee_8_parsers.ino b/tasmota/xdrv_23_zigbee_8_parsers.ino index ffe108c5a..0b1144eb5 100644 --- a/tasmota/xdrv_23_zigbee_8_parsers.ino +++ b/tasmota/xdrv_23_zigbee_8_parsers.ino @@ -653,13 +653,16 @@ int32_t Z_ReceiveAfIncomingMessage(int32_t res, const class SBuffer &buf) { JsonObject& json = jsonBuffer.createObject(); if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_DEFAULT_RESPONSE == zcl_received.getCmdId())) { - zcl_received.parseResponse(); + zcl_received.parseResponse(); // Zigbee general "Degault Response", publish ZbResponse message } else { // Build the ZbReceive json if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_REPORT_ATTRIBUTES == zcl_received.getCmdId())) { - zcl_received.parseRawAttributes(json); + zcl_received.parseRawAttributes(json); // 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); + 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); if (clusterid) { defer_attributes = true; } // don't defer system Cluster=0 messages } else if (zcl_received.isClusterSpecificCommand()) { diff --git a/tasmota/xdrv_23_zigbee_9_impl.ino b/tasmota/xdrv_23_zigbee_9_impl.ino index 5c5181b6d..38627f91f 100644 --- a/tasmota/xdrv_23_zigbee_9_impl.ino +++ b/tasmota/xdrv_23_zigbee_9_impl.ino @@ -32,7 +32,7 @@ TasmotaSerial *ZigbeeSerial = nullptr; const char kZbCommands[] PROGMEM = D_PRFX_ZB "|" // prefix D_CMND_ZIGBEEZNPSEND "|" D_CMND_ZIGBEE_PERMITJOIN "|" D_CMND_ZIGBEE_STATUS "|" D_CMND_ZIGBEE_RESET "|" D_CMND_ZIGBEE_SEND "|" - D_CMND_ZIGBEE_PROBE "|" D_CMND_ZIGBEE_READ "|" D_CMND_ZIGBEEZNPRECEIVE "|" + D_CMND_ZIGBEE_PROBE "|" D_CMND_ZIGBEEZNPRECEIVE "|" D_CMND_ZIGBEE_FORGET "|" D_CMND_ZIGBEE_SAVE "|" D_CMND_ZIGBEE_NAME "|" D_CMND_ZIGBEE_BIND "|" D_CMND_ZIGBEE_UNBIND "|" D_CMND_ZIGBEE_PING "|" D_CMND_ZIGBEE_MODELID "|" D_CMND_ZIGBEE_LIGHT "|" D_CMND_ZIGBEE_RESTORE "|" D_CMND_ZIGBEE_BIND_STATE "|" @@ -42,7 +42,7 @@ const char kZbCommands[] PROGMEM = D_PRFX_ZB "|" // prefix void (* const ZigbeeCommand[])(void) PROGMEM = { &CmndZbZNPSend, &CmndZbPermitJoin, &CmndZbStatus, &CmndZbReset, &CmndZbSend, - &CmndZbProbe, &CmndZbRead, &CmndZbZNPReceive, + &CmndZbProbe, &CmndZbZNPReceive, &CmndZbForget, &CmndZbSave, &CmndZbName, &CmndZbBind, &CmndZbUnbind, &CmndZbPing, &CmndZbModelId, &CmndZbLight, &CmndZbRestore, &CmndZbBindState, @@ -393,9 +393,301 @@ void zigbeeZCLSendStr(uint16_t shortaddr, uint16_t groupaddr, uint8_t endpoint, } } +// Parse "Report" or "Write" attribute +void ZbSendReportWrite(const JsonVariant &val_pubwrite, uint16_t device, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint16_t manuf, bool write) { + SBuffer buf(200); // buffer to store the binary output of attibutes + + const JsonObject &attrs = val_pubwrite.as(); + // iterate on keys + for (JsonObject::const_iterator it=attrs.begin(); it!=attrs.end(); ++it) { + const char *key = it->key; + const JsonVariant &value = it->value; + + uint16_t attr_id = 0xFFFF; + uint16_t cluster_id = 0xFFFF; + uint8_t type_id = Znodata; + + // check if the name has the format "XXXX/YYYY" where XXXX is the cluster, YYYY the attribute id + // alternative "XXXX/YYYY%ZZ" where ZZ is the type (for unregistered attributes) + char * delimiter = strchr(key, '/'); + char * delimiter2 = strchr(key, '%'); + if (delimiter) { + cluster_id = strtoul(key, &delimiter, 16); + if (!delimiter2) { + attr_id = strtoul(delimiter+1, nullptr, 16); + } else { + attr_id = strtoul(delimiter+1, &delimiter2, 16); + type_id = strtoul(delimiter2+1, nullptr, 16); + } + } + // AddLog_P2(LOG_LEVEL_DEBUG, PSTR("cluster_id = 0x%04X, attr_id = 0x%04X"), cluster_id, attr_id); + + // do we already know the type, i.e. attribute and cluster are also known + if (Znodata == type_id) { + // scan attributes to find by name, and retrieve type + for (uint32_t i = 0; i < ARRAY_SIZE(Z_PostProcess); i++) { + const Z_AttributeConverter *converter = &Z_PostProcess[i]; + bool match = false; + uint16_t local_attr_id = pgm_read_word(&converter->attribute); + uint16_t local_cluster_id = CxToCluster(pgm_read_byte(&converter->cluster_short)); + uint8_t local_type_id = pgm_read_byte(&converter->type); + // AddLog_P2(LOG_LEVEL_DEBUG, PSTR("Try cluster = 0x%04X, attr = 0x%04X, type_id = 0x%02X"), local_cluster_id, local_attr_id, local_type_id); + + if (delimiter) { + if ((cluster_id == local_cluster_id) && (attr_id == local_attr_id)) { + type_id = local_type_id; + break; + } + } else if (converter->name) { + // AddLog_P2(LOG_LEVEL_DEBUG, PSTR("Comparing '%s' with '%s'"), attr_name, converter->name); + if (0 == strcasecmp_P(key, converter->name)) { + // match + cluster_id = local_cluster_id; + attr_id = local_attr_id; + type_id = local_type_id; + break; + } + } + } + } + + // AddLog_P2(LOG_LEVEL_DEBUG, PSTR("cluster_id = 0x%04X, attr_id = 0x%04X, type_id = 0x%02X"), cluster_id, attr_id, type_id); + if ((0xFFFF == attr_id) || (0xFFFF == cluster_id)) { + Response_P(PSTR("{\"%s\":\"%s'%s'\"}"), XdrvMailbox.command, PSTR("Unknown attribute "), key); + return; + } + if (Znodata == type_id) { + Response_P(PSTR("{\"%s\":\"%s'%s'\"}"), XdrvMailbox.command, PSTR("Unknown attribute type for attribute "), key); + return; + } + + if (0xFFFF == cluster) { + cluster = cluster_id; // set the cluster for this packet + } else if (cluster != cluster_id) { + ResponseCmndChar_P(PSTR("No more than one cluster id per command")); + return; + } + // push the value in the buffer + int32_t res = encodeSingleAttribute(buf, value, attr_id, type_id); + if (res < 0) { + Response_P(PSTR("{\"%s\":\"%s'%s' 0x%02X\"}"), XdrvMailbox.command, PSTR("Unsupported attribute type "), key, type_id); + return; + } + } + + // did we have any attribute? + if (0 == buf.len()) { + ResponseCmndChar_P(PSTR("No attribute in list")); + return; + } + + // all good, send the packet + ZigbeeZCLSend_Raw(device, groupaddr, cluster, endpoint, write ? ZCL_WRITE_ATTRIBUTES : ZCL_REPORT_ATTRIBUTES, false /* not cluster specific */, manuf, buf.getBuffer(), buf.len(), false /* noresponse */, zigbee_devices.getNextSeqNumber(device)); + ResponseCmndDone(); +} + +// Parse the "Send" attribute and send the command +void ZbSendSend(const JsonVariant &val_cmd, uint16_t device, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint16_t manuf) { + uint8_t cmd = 0; + String cmd_str = ""; // the actual low-level command, either specified or computed + const char *cmd_s; // pointer to payload string + bool clusterSpecific = true; + + static char delim[] = ", "; // delimiters for parameters + // probe the type of the argument + // If JSON object, it's high level commands + // If String, it's a low level command + if (val_cmd.is()) { + // we have a high-level command + const JsonObject &cmd_obj = val_cmd.as(); + int32_t cmd_size = cmd_obj.size(); + if (cmd_size > 1) { + Response_P(PSTR("Only 1 command allowed (%d)"), cmd_size); + return; + } else if (1 == cmd_size) { + // We have exactly 1 command, parse it + JsonObject::const_iterator it = cmd_obj.begin(); // just get the first key/value + String key = it->key; + const JsonVariant& value = it->value; + uint32_t x = 0, y = 0, z = 0; + uint16_t cmd_var; + + const __FlashStringHelper* tasmota_cmd = zigbeeFindCommand(key.c_str(), &cluster, &cmd_var); + if (tasmota_cmd) { + cmd_str = tasmota_cmd; + } else { + Response_P(PSTR("Unrecognized zigbee command: %s"), key.c_str()); + return; + } + + // parse the JSON value, depending on its type fill in x,y,z + if (value.is()) { + x = value.as() ? 1 : 0; + } else if (value.is()) { + x = value.as(); + } else { + // if non-bool or non-int, trying char* + const char *s_const = value.as(); + if (s_const != nullptr) { + char s[strlen(s_const)+1]; + strcpy(s, s_const); + if ((nullptr != s) && (0x00 != *s)) { // ignore any null or empty string, could represent 'null' json value + char *sval = strtok(s, delim); + if (sval) { + x = ZigbeeAliasOrNumber(sval); + sval = strtok(nullptr, delim); + if (sval) { + y = ZigbeeAliasOrNumber(sval); + sval = strtok(nullptr, delim); + if (sval) { + z = ZigbeeAliasOrNumber(sval); + } + } + } + } + } + } + + //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZbSend: command_template = %s"), cmd_str.c_str()); + if (0xFF == cmd_var) { // if command number is a variable, replace it with x + cmd = x; + x = y; // and shift other variables + y = z; + } else { + cmd = cmd_var; // or simply copy the cmd number + } + cmd_str = zigbeeCmdAddParams(cmd_str.c_str(), x, y, z); // fill in parameters + //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZbSend: command_final = %s"), cmd_str.c_str()); + cmd_s = cmd_str.c_str(); + } else { + // we have zero command, pass through until last error for missing command + } + } else if (val_cmd.is()) { + // low-level command + cmd_str = val_cmd.as(); + // Now parse the string to extract cluster, command, and payload + // Parse 'cmd' in the form "AAAA_BB/CCCCCCCC" or "AAAA!BB/CCCCCCCC" + // where AA is the cluster number, BBBB the command number, CCCC... the payload + // First delimiter is '_' for a global command, or '!' for a cluster specific command + const char * data = cmd_str.c_str(); + cluster = parseHex(&data, 4); + + // delimiter + if (('_' == *data) || ('!' == *data)) { + if ('_' == *data) { clusterSpecific = false; } + data++; + } else { + ResponseCmndChar_P(PSTR("Wrong delimiter for payload")); + return; + } + // parse cmd number + cmd = parseHex(&data, 2); + + // move to end of payload + // delimiter is optional + if ('/' == *data) { data++; } // skip delimiter + + cmd_s = data; + } else { + // we have an unsupported command type, just ignore it and fallback to missing command + } + + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZigbeeZCLSend device: 0x%04X, group: 0x%04X, endpoint:%d, cluster:0x%04X, cmd:0x%02X, send:\"%s\""), + device, groupaddr, endpoint, cluster, cmd, cmd_s); + zigbeeZCLSendStr(device, groupaddr, endpoint, clusterSpecific, manuf, cluster, cmd, cmd_s); + ResponseCmndDone(); +} + + +// Parse the "Send" attribute and send the command +void ZbSendRead(const JsonVariant &val_attr, uint16_t device, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint16_t manuf) { + // ZbSend {"Device":"0xF289","Cluster":0,"Endpoint":3,"Read":5} + // ZbSend {"Device":"0xF289","Cluster":"0x0000","Endpoint":"0x0003","Read":"0x0005"} + // ZbSend {"Device":"0xF289","Cluster":0,"Endpoint":3,"Read":[5,6,7,4]} + // ZbSend {"Device":"0xF289","Endpoint":3,"Read":{"ModelId":true}} + // ZbSend {"Device":"0xF289","Read":{"ModelId":true}} + + // params + size_t attrs_len = 0; + uint8_t* attrs = nullptr; // empty string is valid + + uint16_t val = strToUInt(val_attr); + if (val_attr.is()) { + const JsonArray& attr_arr = val_attr.as(); + attrs_len = attr_arr.size() * 2; + attrs = new uint8_t[attrs_len]; + + uint32_t i = 0; + for (auto value : attr_arr) { + uint16_t val = strToUInt(value); + attrs[i++] = val & 0xFF; + attrs[i++] = val >> 8; + } + } else if (val_attr.is()) { + const JsonObject& attr_obj = val_attr.as(); + attrs_len = attr_obj.size() * 2; + attrs = new uint8_t[attrs_len]; + uint32_t actual_attr_len = 0; + + // iterate on keys + for (JsonObject::const_iterator it=attr_obj.begin(); it!=attr_obj.end(); ++it) { + const char *key = it->key; + // const JsonVariant &value = it->value; // we don't need the value here, only keys are relevant + + bool found = false; + // scan attributes to find by name, and retrieve type + for (uint32_t i = 0; i < ARRAY_SIZE(Z_PostProcess); i++) { + const Z_AttributeConverter *converter = &Z_PostProcess[i]; + bool match = false; + uint16_t local_attr_id = pgm_read_word(&converter->attribute); + uint16_t local_cluster_id = CxToCluster(pgm_read_byte(&converter->cluster_short)); + // uint8_t local_type_id = pgm_read_byte(&converter->type); + + if ((converter->name) && (0 == strcasecmp_P(key, converter->name))) { + // match name + // check if there is a conflict with cluster + // TODO + attrs[actual_attr_len++] = local_attr_id & 0xFF; + attrs[actual_attr_len++] = local_attr_id >> 8; + found = true; + break; // found, exit loop + } + } + if (!found) { + AddLog_P2(LOG_LEVEL_INFO, PSTR("ZIG: Unknown attribute name (ignored): %s"), key); + } + } + + attrs_len = actual_attr_len; + } else { + attrs_len = 2; + attrs = new uint8_t[attrs_len]; + attrs[0] = val & 0xFF; // little endian + attrs[1] = val >> 8; + } + + if (attrs_len > 0) { + ZigbeeZCLSend_Raw(device, groupaddr, cluster, endpoint, ZCL_READ_ATTRIBUTES, false, manuf, attrs, attrs_len, true /* we do want a response */, zigbee_devices.getNextSeqNumber(device)); + ResponseCmndDone(); + } else { + ResponseCmndChar_P(PSTR("Missing parameters")); + } + + if (attrs) { delete[] attrs; } +} + // // Command `ZbSend` // +// Examples: +// ZbSend {"Device":"0x0000","Endpoint":1,"Write":{"0006/0000":0}} +// ZbSend {"Device":"0x0000","Endpoint":1,"Write":{"Power":0}} +// ZbSend {"Device":"0x0000","Endpoint":1,"Write":{"AqaraRotate":0}} +// ZbSend {"Device":"0x0000","Endpoint":1,"Write":{"AqaraRotate":12.5}} +// ZbSend {"Device":"0x0000","Endpoint":1,"Write":{"006/0000%39":12.5}} +// ZbSend {"Device":"0x0000","Endpoint":1,"Write":{"AnalogInApplicationType":1000000}} +// ZbSend {"Device":"0x0000","Endpoint":1,"Write":{"TimeZone":-1000000}} +// ZbSend {"Device":"0x0000","Endpoint":1,"Write":{"Manufacturer":"Tasmota","ModelId":"Tasmota Z2T Router"}} void CmndZbSend(void) { // ZbSend { "device":"0x1234", "endpoint":"0x03", "send":{"Power":1} } // ZbSend { "device":"0x1234", "endpoint":"0x03", "send":{"Power":"3"} } @@ -405,7 +697,7 @@ void CmndZbSend(void) { // ZbSend { "device":"0x1234", "endpoint":"0x03", "send":{"Power":true} } // ZbSend { "device":"0x1234", "endpoint":"0x03", "send":{"Power":"true"} } // ZbSend { "device":"0x1234", "endpoint":"0x03", "send":{"ShutterClose":null} } - // ZbSend { "devicse":"0x1234", "endpoint":"0x03", "send":{"Power":1} } + // ZbSend { "device":"0x1234", "endpoint":"0x03", "send":{"Power":1} } // ZbSend { "device":"0x1234", "endpoint":"0x03", "send":{"Color":"1,2"} } // ZbSend { "device":"0x1234", "endpoint":"0x03", "send":{"Color":"0x1122,0xFFEE"} } if (zigbee.init_phase) { ResponseCmndChar_P(PSTR(D_ZIGBEE_NOT_STARTED)); return; } @@ -414,26 +706,21 @@ void CmndZbSend(void) { if (!json.success()) { ResponseCmndChar_P(PSTR(D_JSON_INVALID_JSON)); return; } // params - static char delim[] = ", "; // delimiters for parameters - uint16_t device = BAD_SHORTADDR; // 0x0000 is local, so considered invalid - uint16_t groupaddr = 0x0000; // group address - uint8_t endpoint = 0x00; // 0x00 is invalid for the dst endpoint - uint16_t manuf = 0x0000; // Manuf Id in ZCL frame - // Command elements - uint16_t cluster = 0; - uint8_t cmd = 0; - String cmd_str = ""; // the actual low-level command, either specified or computed - const char *cmd_s; // pointer to payload string - bool clusterSpecific = true; + uint16_t device = BAD_SHORTADDR; // BAD_SHORTADDR is broadcast, so considered invalid + uint16_t groupaddr = 0x0000; // group address valid only if device == BAD_SHORTADDR + uint16_t cluster = 0xFFFF; // no default + uint8_t endpoint = 0x00; // 0x00 is invalid for the dst endpoint + uint16_t manuf = 0x0000; // Manuf Id in ZCL frame - // parse JSON - const JsonVariant &val_device = GetCaseInsensitive(json, PSTR("Device")); + + // parse "Device" and "Group" + const JsonVariant &val_device = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_DEVICE)); if (nullptr != &val_device) { device = zigbee_devices.parseDeviceParam(val_device.as()); if (BAD_SHORTADDR == device) { ResponseCmndChar_P(PSTR("Invalid parameter")); return; } } if (BAD_SHORTADDR == device) { // if not found, check if we have a group - const JsonVariant &val_group = GetCaseInsensitive(json, PSTR("Group")); + const JsonVariant &val_group = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_GROUP)); if (nullptr != &val_group) { groupaddr = strToUInt(val_group); } else { // no device nor group @@ -442,116 +729,54 @@ void CmndZbSend(void) { } } - const JsonVariant &val_endpoint = GetCaseInsensitive(json, PSTR("Endpoint")); + // read other parameters + const JsonVariant &val_cluster = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_CLUSTER)); + if (nullptr != &val_cluster) { cluster = strToUInt(val_cluster); } + const JsonVariant &val_endpoint = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_ENDPOINT)); if (nullptr != &val_endpoint) { endpoint = strToUInt(val_endpoint); } - const JsonVariant &val_manuf = GetCaseInsensitive(json, PSTR("Manuf")); + const JsonVariant &val_manuf = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_MANUF)); if (nullptr != &val_manuf) { manuf = strToUInt(val_manuf); } - const JsonVariant &val_cmd = GetCaseInsensitive(json, PSTR("Send")); + + // infer endpoint + if (BAD_SHORTADDR == device) { + endpoint = 0xFF; // endpoint not used for group addresses + } else if (0 == endpoint) { + endpoint = zigbee_devices.findFirstEndpoint(device); + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZIG: guessing endpoint %d"), endpoint); + } + + const JsonVariant &val_cmd = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_SEND)); + const JsonVariant &val_read = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_READ)); + const JsonVariant &val_write = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_WRITE)); + const JsonVariant &val_publish = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_REPORT)); + uint32_t multi_cmd = (nullptr != &val_cmd) + (nullptr != &val_read) + (nullptr != &val_write) + (nullptr != &val_publish); + if (multi_cmd > 1) { + ResponseCmndChar_P(PSTR("Can only have one of: 'Send', 'Read', 'Write' or 'Report'")); + return; + } + if (nullptr != &val_cmd) { - // probe the type of the argument - // If JSON object, it's high level commands - // If String, it's a low level command - if (val_cmd.is()) { - // we have a high-level command - const JsonObject &cmd_obj = val_cmd.as(); - int32_t cmd_size = cmd_obj.size(); - if (cmd_size > 1) { - Response_P(PSTR("Only 1 command allowed (%d)"), cmd_size); - return; - } else if (1 == cmd_size) { - // We have exactly 1 command, parse it - JsonObject::const_iterator it = cmd_obj.begin(); // just get the first key/value - String key = it->key; - const JsonVariant& value = it->value; - uint32_t x = 0, y = 0, z = 0; - uint16_t cmd_var; - - const __FlashStringHelper* tasmota_cmd = zigbeeFindCommand(key.c_str(), &cluster, &cmd_var); - if (tasmota_cmd) { - cmd_str = tasmota_cmd; - } else { - Response_P(PSTR("Unrecognized zigbee command: %s"), key.c_str()); - return; - } - - // parse the JSON value, depending on its type fill in x,y,z - if (value.is()) { - x = value.as() ? 1 : 0; - } else if (value.is()) { - x = value.as(); - } else { - // if non-bool or non-int, trying char* - const char *s_const = value.as(); - if (s_const != nullptr) { - char s[strlen(s_const)+1]; - strcpy(s, s_const); - if ((nullptr != s) && (0x00 != *s)) { // ignore any null or empty string, could represent 'null' json value - char *sval = strtok(s, delim); - if (sval) { - x = ZigbeeAliasOrNumber(sval); - sval = strtok(nullptr, delim); - if (sval) { - y = ZigbeeAliasOrNumber(sval); - sval = strtok(nullptr, delim); - if (sval) { - z = ZigbeeAliasOrNumber(sval); - } - } - } - } - } - } - - //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZbSend: command_template = %s"), cmd_str.c_str()); - if (0xFF == cmd_var) { // if command number is a variable, replace it with x - cmd = x; - x = y; // and shift other variables - y = z; - } else { - cmd = cmd_var; // or simply copy the cmd number - } - cmd_str = zigbeeCmdAddParams(cmd_str.c_str(), x, y, z); // fill in parameters - //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZbSend: command_final = %s"), cmd_str.c_str()); - cmd_s = cmd_str.c_str(); - } else { - // we have zero command, pass through until last error for missing command - } - } else if (val_cmd.is()) { - // low-level command - cmd_str = val_cmd.as(); - // Now parse the string to extract cluster, command, and payload - // Parse 'cmd' in the form "AAAA_BB/CCCCCCCC" or "AAAA!BB/CCCCCCCC" - // where AA is the cluster number, BBBB the command number, CCCC... the payload - // First delimiter is '_' for a global command, or '!' for a cluster specific command - const char * data = cmd_str.c_str(); - cluster = parseHex(&data, 4); - - // delimiter - if (('_' == *data) || ('!' == *data)) { - if ('_' == *data) { clusterSpecific = false; } - data++; - } else { - ResponseCmndChar_P(PSTR("Wrong delimiter for payload")); - return; - } - // parse cmd number - cmd = parseHex(&data, 2); - - // move to end of payload - // delimiter is optional - if ('/' == *data) { data++; } // skip delimiter - - cmd_s = data; - } else { - // we have an unsupported command type, just ignore it and fallback to missing command + // "Send":{...commands...} + ZbSendSend(val_cmd, device, groupaddr, cluster, endpoint, manuf); + } else if (nullptr != &val_read) { + // "Read":{...attributes...}, "Read":attribute or "Read":[...attributes...] + ZbSendRead(val_read, device, groupaddr, cluster, endpoint, manuf); + } else if (nullptr != &val_write) { + if ((0 == endpoint) || (!val_write.is())) { + ResponseCmndChar_P(PSTR("Missing parameters")); + return; } - - AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZigbeeZCLSend device: 0x%04X, group: 0x%04X, endpoint:%d, cluster:0x%04X, cmd:0x%02X, send:\"%s\""), - device, groupaddr, endpoint, cluster, cmd, cmd_s); - zigbeeZCLSendStr(device, groupaddr, endpoint, clusterSpecific, manuf, cluster, cmd, cmd_s); - ResponseCmndDone(); + // "Write":{...attributes...} + ZbSendReportWrite(val_write, device, groupaddr, cluster, endpoint, manuf, true /* write */); + } else if (nullptr != &val_publish) { + if ((0 == endpoint) || (!val_publish.is())) { + ResponseCmndChar_P(PSTR("Missing parameters")); + return; + } + // "Report":{...attributes...} + ZbSendReportWrite(val_publish, device, groupaddr, cluster, endpoint, manuf, false /* report */); } else { - Response_P(PSTR("Missing zigbee 'Send'")); + Response_P(PSTR("Missing zigbee 'Send', 'Write' or 'Report'")); return; } } @@ -570,7 +795,6 @@ void ZbBindUnbind(bool unbind) { // false = bind, true = unbind if (!json.success()) { ResponseCmndChar_P(PSTR(D_JSON_INVALID_JSON)); return; } // params - // static char delim[] = ", "; // delimiters for parameters uint16_t srcDevice = BAD_SHORTADDR; // BAD_SHORTADDR is broadcast, so considered invalid uint16_t dstDevice = BAD_SHORTADDR; // BAD_SHORTADDR is broadcast, so considered invalid uint64_t dstLongAddr = 0; @@ -582,7 +806,7 @@ void ZbBindUnbind(bool unbind) { // false = bind, true = unbind // Information about source device: "Device", "Endpoint", "Cluster" // - the source endpoint must have a known IEEE address - const JsonVariant &val_device = GetCaseInsensitive(json, PSTR("Device")); + const JsonVariant &val_device = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_DEVICE)); if (nullptr != &val_device) { srcDevice = zigbee_devices.parseDeviceParam(val_device.as()); } @@ -591,10 +815,10 @@ void ZbBindUnbind(bool unbind) { // false = bind, true = unbind uint64_t srcLongAddr = zigbee_devices.getDeviceLongAddr(srcDevice); if (0 == srcLongAddr) { ResponseCmndChar_P(PSTR("Unknown source IEEE address")); return; } // look for source endpoint - const JsonVariant &val_endpoint = GetCaseInsensitive(json, PSTR("Endpoint")); + const JsonVariant &val_endpoint = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_ENDPOINT)); if (nullptr != &val_endpoint) { endpoint = strToUInt(val_endpoint); } // look for source cluster - const JsonVariant &val_cluster = GetCaseInsensitive(json, PSTR("Cluster")); + const JsonVariant &val_cluster = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_CLUSTER)); if (nullptr != &val_cluster) { cluster = strToUInt(val_cluster); } // Either Device address @@ -885,90 +1109,6 @@ void CmndZbRestore(void) { ResponseCmndDone(); } -// -// Command `ZbRead` -// Send an attribute read command to a device, specifying cluster and list of attributes -// -void CmndZbRead(void) { - // ZbRead {"Device":"0xF289","Cluster":0,"Endpoint":3,"Attr":5} - // ZbRead {"Device":"0xF289","Cluster":"0x0000","Endpoint":"0x0003","Attr":"0x0005"} - // ZbRead {"Device":"0xF289","Cluster":0,"Endpoint":3,"Attr":[5,6,7,4]} - if (zigbee.init_phase) { ResponseCmndChar_P(PSTR(D_ZIGBEE_NOT_STARTED)); return; } - DynamicJsonBuffer jsonBuf; - JsonObject &json = jsonBuf.parseObject((const char*) XdrvMailbox.data); - if (!json.success()) { ResponseCmndChar_P(PSTR(D_JSON_INVALID_JSON)); return; } - - // params - uint16_t device = BAD_SHORTADDR; // BAD_SHORTADDR is broadcast, so considered invalid - uint16_t groupaddr = 0x0000; // if 0x0000 ignore group adress - uint16_t cluster = 0x0000; // default to general cluster - uint8_t endpoint = 0x00; // 0x00 is invalid for the dst endpoint - uint16_t manuf = 0x0000; // Manuf Id in ZCL frame - size_t attrs_len = 0; - uint8_t* attrs = nullptr; // empty string is valid - - const JsonVariant &val_device = GetCaseInsensitive(json, PSTR("Device")); - if (nullptr != &val_device) { - device = zigbee_devices.parseDeviceParam(val_device.as()); - if (BAD_SHORTADDR == device) { ResponseCmndChar_P(PSTR("Invalid parameter")); return; } - } - if (BAD_SHORTADDR == device) { // if not found, check if we have a group - const JsonVariant &val_group = GetCaseInsensitive(json, PSTR("Group")); - if (nullptr != &val_group) { - groupaddr = strToUInt(val_group); - } else { // no device nor group - ResponseCmndChar_P(PSTR("Unknown device")); - return; - } - } - - const JsonVariant &val_cluster = GetCaseInsensitive(json, PSTR("Cluster")); - if (nullptr != &val_cluster) { cluster = strToUInt(val_cluster); } - const JsonVariant &val_endpoint = GetCaseInsensitive(json, PSTR("Endpoint")); - if (nullptr != &val_endpoint) { endpoint = strToUInt(val_endpoint); } - const JsonVariant &val_manuf = GetCaseInsensitive(json, PSTR("Manuf")); - if (nullptr != &val_manuf) { manuf = strToUInt(val_manuf); } - - const JsonVariant &val_attr = GetCaseInsensitive(json, PSTR("Read")); - if (nullptr != &val_attr) { - uint16_t val = strToUInt(val_attr); - if (val_attr.is()) { - const JsonArray& attr_arr = val_attr.as(); - attrs_len = attr_arr.size() * 2; - attrs = new uint8_t[attrs_len]; - - uint32_t i = 0; - for (auto value : attr_arr) { - uint16_t val = strToUInt(value); - attrs[i++] = val & 0xFF; - attrs[i++] = val >> 8; - } - } else { - attrs_len = 2; - attrs = new uint8_t[attrs_len]; - attrs[0] = val & 0xFF; // little endian - attrs[1] = val >> 8; - } - } - - if ((0 == endpoint) && (device)) { // try to compute the endpoint - endpoint = zigbee_devices.findFirstEndpoint(device); - AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZbRead: guessing endpoint 0x%02X"), endpoint); - } - if (BAD_SHORTADDR == device) { - endpoint = 0xFF; // endpoint not used for group addresses - } - - if ((0 != endpoint) && (attrs_len > 0)) { - ZigbeeZCLSend_Raw(device, groupaddr, cluster, endpoint, ZCL_READ_ATTRIBUTES, false, manuf, attrs, attrs_len, true /* we do want a response */, zigbee_devices.getNextSeqNumber(device)); - ResponseCmndDone(); - } else { - ResponseCmndChar_P(PSTR("Missing parameters")); - } - - if (attrs) { delete[] attrs; } -} - // // Command `ZbPermitJoin` // Allow or Deny pairing of new Zigbee devices