diff --git a/tasmota/CHANGELOG.md b/tasmota/CHANGELOG.md index c8cf0ab3a..ef9ce17f4 100644 --- a/tasmota/CHANGELOG.md +++ b/tasmota/CHANGELOG.md @@ -6,6 +6,7 @@ - Add initial support for Sensors AHT10 and AHT15 by Martin Wagner (#7596) - Add support for Wemos Motor Shield V1 by Denis Sborets (#7764) - Fix Zigbee auto-increment transaction number (#7757) +- Add Zigbee enhanced commands decoding, added ``ZbPing`` ### 8.1.0.8 20200212 diff --git a/tasmota/i18n.h b/tasmota/i18n.h index c7930e79b..ab7d18ce0 100644 --- a/tasmota/i18n.h +++ b/tasmota/i18n.h @@ -496,6 +496,8 @@ #define D_JSON_ZIGBEE_RECEIVED "ZbReceived" #define D_JSON_ZIGBEE_RECEIVED_LEGACY "ZigbeeReceived" #define D_CMND_ZIGBEE_BIND "Bind" +#define D_CMND_ZIGBEE_PING "Ping" + #define D_JSON_ZIGBEE_PING "ZbPing" // Commands xdrv_25_A4988_Stepper.ino #define D_CMND_MOTOR "MOTOR" diff --git a/tasmota/xdrv_23_zigbee_3_devices.ino b/tasmota/xdrv_23_zigbee_3_devices.ino index 78c192cc1..71752f4e3 100644 --- a/tasmota/xdrv_23_zigbee_3_devices.ino +++ b/tasmota/xdrv_23_zigbee_3_devices.ino @@ -19,6 +19,10 @@ #ifdef USE_ZIGBEE +#ifndef ZIGBEERECEIVED +#define ZIGBEERECEIVED 1 +#endif + #include #include @@ -725,18 +729,22 @@ void Z_Devices::jsonPublishFlush(uint16_t shortaddr) { Response_P(PSTR("{\"" D_JSON_ZIGBEE_RECEIVED "\":{\"%s\":%s}}"), fname->c_str(), msg.c_str()); MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR)); XdrvRulesProcess(); +#if ZIGBEERECEIVED // DEPRECATED TODO Response_P(PSTR("{\"" D_JSON_ZIGBEE_RECEIVED_LEGACY "\":{\"%s\":%s}}"), fname->c_str(), msg.c_str()); MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR)); XdrvRulesProcess(); +#endif } else { Response_P(PSTR("{\"" D_JSON_ZIGBEE_RECEIVED "\":{\"0x%04X\":%s}}"), shortaddr, msg.c_str()); MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR)); XdrvRulesProcess(); +#if ZIGBEERECEIVED // DEPRECATED TODO Response_P(PSTR("{\"" D_JSON_ZIGBEE_RECEIVED_LEGACY "\":{\"0x%04X\":%s}}"), shortaddr, msg.c_str()); MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR)); XdrvRulesProcess(); +#endif } // MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR)); // XdrvRulesProcess(); diff --git a/tasmota/xdrv_23_zigbee_5_converters.ino b/tasmota/xdrv_23_zigbee_5_converters.ino index d97e5e7d4..4debd73fd 100644 --- a/tasmota/xdrv_23_zigbee_5_converters.ino +++ b/tasmota/xdrv_23_zigbee_5_converters.ino @@ -486,18 +486,8 @@ void ZCLFrame::parseReadAttributes(JsonObject& json, uint8_t offset) { // Parse non-normalized attributes -// The key is "s_" followed by 16 bits clusterId, "_" followed by 8 bits command id void ZCLFrame::parseClusterSpecificCommand(JsonObject& json, uint8_t offset) { - uint32_t i = offset; - uint32_t len = _payload.len(); - - char attrid_str[12]; - snprintf_P(attrid_str, sizeof(attrid_str), PSTR("%04X!%02X"), _cluster_id, _cmd_id); - - char hex_char[_payload.len()*2+2]; - ToHex_P((unsigned char*)_payload.getBuffer(), _payload.len(), hex_char, sizeof(hex_char)); - - json[attrid_str] = hex_char; + convertClusterSpecific(json, _cluster_id, _cmd_id, _frame_control.b.direction, _payload); } // return value: diff --git a/tasmota/xdrv_23_zigbee_6_commands.ino b/tasmota/xdrv_23_zigbee_6_commands.ino index 9d6f38b95..5c3e3fc1b 100644 --- a/tasmota/xdrv_23_zigbee_6_commands.ino +++ b/tasmota/xdrv_23_zigbee_6_commands.ino @@ -19,34 +19,81 @@ #ifdef USE_ZIGBEE -//typedef int32_t (*Z_AttrConverter)(uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const char *new_name, void * param); typedef struct Z_CommandConverter { const char * tasmota_cmd; - const char * zcl_cmd; + uint16_t cluster; + uint8_t cmd; // normally 8 bits, 0xFF means it's a parameter + uint8_t direction; // direction of the command. 0x01 client->server, 0x02 server->client, 0x03 both + const char * param; } Z_CommandConverter; +typedef struct Z_XYZ_Var { // Holds values for vairables X, Y and Z + uint32_t x = 0; + uint32_t y = 0; + uint32_t z = 0; + uint8_t x_type = 0; // 0 = no value, 1 = 1 bytes, 2 = 2 bytes + uint8_t y_type = 0; + uint8_t z_type = 0; +} Z_XYZ_Var; + // list of post-processing directives const Z_CommandConverter Z_Commands[] = { - { "Power", "0006!xx" }, // 0=Off, 1=On, 2=Toggle - { "Dimmer", "0008!04/xx0A00" }, // Move to Level with On/Off, xx=0..254 (255 is invalid) - { "Dimmer+", "0008!06/001902" }, // Step up by 10%, 0.2 secs - { "Dimmer-", "0008!06/011902" }, // Step down by 10%, 0.2 secs - { "DimmerStop", "0008!03" }, // Stop any Dimmer animation - { "ResetAlarm", "0009!00/xxyyyy" }, // Reset alarm (alarm code + cluster identifier) - { "ResetAllAlarms","0009!01" }, // Reset all alarms - { "Hue", "0300!00/xx000A00" }, // Move to Hue, shortest time, 1s - { "Sat", "0300!03/xx0A00" }, // Move to Sat - { "HueSat", "0300!06/xxyy0A00" }, // Hue, Sat - { "Color", "0300!07/xxxxyyyy0A00" }, // x, y (uint16) - { "CT", "0300!0A/xxxx0A00" }, // Color Temperature Mireds (uint16) - { "Shutter", "0102!xx" }, - { "ShutterOpen", "0102!00" }, - { "ShutterClose", "0102!01" }, - { "ShutterStop", "0102!02" }, - { "ShutterLift", "0102!05xx" }, // Lift percentage, 0%=open, 100%=closed - { "ShutterTilt", "0102!08xx" }, // Tilt percentage + // Group adress commands + { "AddGroup", 0x0004, 0x00, 0x01, "xxxx00" }, // Add group id, group name is not supported + { "ViewGroup", 0x0004, 0x01, 0x01, "xxxx" }, // Ask for the group name + { "GetGroup", 0x0004, 0x02, 0x01, "01xxxx" }, // Get one group membership + { "GetAllGroups", 0x0004, 0x02, 0x01, "00" }, // Get all groups membership + { "RemoveGroup", 0x0004, 0x03, 0x01, "xxxx" }, // Remove one group + { "RemoveAllGroups",0x0004, 0x04, 0x01, "" }, // Remove all groups + // Light & Shutter commands + { "Power", 0x0006, 0xFF, 0x01, "" }, // 0=Off, 1=On, 2=Toggle + { "Dimmer", 0x0008, 0x04, 0x01, "xx0A00" }, // Move to Level with On/Off, xx=0..254 (255 is invalid) + { "Dimmer+", 0x0008, 0x06, 0x01, "001902" }, // Step up by 10%, 0.2 secs + { "Dimmer-", 0x0008, 0x06, 0x01, "011902" }, // Step down by 10%, 0.2 secs + { "DimmerStop", 0x0008, 0x03, 0x01, "" }, // Stop any Dimmer animation + { "ResetAlarm", 0x0009, 0x00, 0x01, "xxyyyy" }, // Reset alarm (alarm code + cluster identifier) + { "ResetAllAlarms", 0x0009, 0x01, 0x01, "" }, // Reset all alarms + { "Hue", 0x0300, 0x00, 0x01, "xx000A00" }, // Move to Hue, shortest time, 1s + { "Sat", 0x0300, 0x03, 0x01, "xx0A00" }, // Move to Sat + { "HueSat", 0x0300, 0x06, 0x01, "xxyy0A00" }, // Hue, Sat + { "Color", 0x0300, 0x07, 0x01, "xxxxyyyy0A00" }, // x, y (uint16) + { "CT", 0x0300, 0x0A, 0x01, "xxxx0A00" }, // Color Temperature Mireds (uint16) + { "ShutterOpen", 0x0102, 0x00, 0x01, "" }, + { "ShutterClose", 0x0102, 0x01, 0x01, "" }, + { "ShutterStop", 0x0102, 0x02, 0x01, "" }, + { "ShutterLift", 0x0102, 0x05, 0x01, "xx" }, // Lift percentage, 0%=open, 100%=closed + { "ShutterTilt", 0x0102, 0x08, 0x01, "xx" }, // Tilt percentage + { "Shutter", 0x0102, 0xFF, 0x01, "" }, + // Blitzwolf PIR + { "Occupancy", 0xEF00, 0x01, 0x01, "xx"}, // Specific decoder for Blitzwolf PIR, empty name means special treatment + // Decoders only - normally not used to send, and names may be masked by previous definitions + { "Dimmer", 0x0008, 0x00, 0x01, "xx" }, + { "DimmerMove", 0x0008, 0x01, 0x01, "xx0A" }, + { "DimmerStep", 0x0008, 0x02, 0x01, "xx190A00" }, + { "DimmerMove", 0x0008, 0x05, 0x01, "xx0A" }, + { "Dimmer+", 0x0008, 0x06, 0x01, "00" }, + { "Dimmer-", 0x0008, 0x06, 0x01, "01" }, + { "DimmerStop", 0x0008, 0x07, 0x01, "" }, + { "HueMove", 0x0300, 0x01, 0x01, "xx19" }, + { "HueStep", 0x0300, 0x02, 0x01, "xx190A00" }, + { "SatMove", 0x0300, 0x04, 0x01, "xx19" }, + { "SatStep", 0x0300, 0x05, 0x01, "xx190A" }, + { "ColorMove", 0x0300, 0x08, 0x01, "xxxxyyyy" }, + { "ColorStep", 0x0300, 0x09, 0x01, "xxxxyyyy0A00" }, + // Tradfri + { "ArrowClick", 0x0005, 0x07, 0x01, "xx" }, // xx == 0x01 = left, 0x00 = right + { "ArrowHold", 0x0005, 0x08, 0x01, "xx" }, // xx == 0x01 = left, 0x00 = right + { "ArrowRelease", 0x0005, 0x09, 0x01, "" }, + // IAS - Intruder Alarm System + leak/fire detection + { "ZoneStatusChange",0x0500, 0x00, 0x02, "xxxxyyzz" }, // xxxx = zone status, yy = extended status, zz = zone id, Delay is ignored + // responses for Group cluster commands + { "AddGroupResp", 0x0004, 0x00, 0x02, "xxyyyy" }, // xx = status, yy = group id + { "ViewGroupResp", 0x0004, 0x01, 0x02, "xxyyyy" }, // xx = status, yy = group id, name ignored + { "GetGroupResp", 0x0004, 0x02, 0x02, "xxyyzzzz" }, // xx = capacity, yy = count, zzzz = first group id, following groups ignored + { "RemoveGroup", 0x0004, 0x03, 0x02, "xxyyyy" }, // xx = status, yy = group id }; + #define ZLE(x) ((x) & 0xFF), ((x) >> 8) // Little Endian // Below are the attributes we wand to read from each cluster @@ -55,6 +102,7 @@ const uint8_t CLUSTER_0008[] = { ZLE(0x0000) }; // CurrentLevel const uint8_t CLUSTER_0009[] = { ZLE(0x0000) }; // AlarmCount const uint8_t CLUSTER_0300[] = { ZLE(0x0000), ZLE(0x0001), ZLE(0x0003), ZLE(0x0004), ZLE(0x0007) }; // Hue, Sat, X, Y, CT +// This callback is registered after a cluster specific command and sends a read command for the same cluster int32_t Z_ReadAttrCallback(uint16_t shortaddr, uint16_t cluster, uint16_t endpoint, uint32_t value) { size_t attrs_len = 0; const uint8_t* attrs = nullptr; @@ -82,7 +130,6 @@ int32_t Z_ReadAttrCallback(uint16_t shortaddr, uint16_t cluster, uint16_t endpoi } } - // set a timer to read back the value in the future void zigbeeSetCommandTimer(uint16_t shortaddr, uint16_t cluster, uint16_t endpoint) { uint32_t wait_ms = 0; @@ -105,22 +152,182 @@ void zigbeeSetCommandTimer(uint16_t shortaddr, uint16_t cluster, uint16_t endpoi } } -const __FlashStringHelper* zigbeeFindCommand(const char *command) { - char parm_uc[16]; // used to convert JSON keys to uppercase +// returns true if char is 'x', 'y' or 'z' +inline bool isXYZ(char c) { + return (c >= 'x') && (c <= 'z'); +} + +// returns the Hex value of a digit [0-9A-Fa-f] +// return: 0x00-0x0F +// or -1 if cannot be parsed +inline int8_t hexValue(char c) { + if ((c >= '0') && (c <= '9')) { + return c - '0'; + } + if ((c >= 'A') && (c <= 'F')) { + return 10 + c - 'A'; + } + if ((c >= 'a') && (c <= 'f')) { + return 10 + c - 'a'; + } + return -1; +} + +// Parse a Big Endian suite of max_len digits, or stops when a non-hex digit is found +uint32_t parseHex_P(const char **data, size_t max_len = 8) { + uint32_t ret = 0; + for (uint32_t i = 0; i < max_len; i++) { + int8_t v = hexValue(pgm_read_byte(*data)); + if (v < 0) { break; } // non hex digit, we stop parsing + ret = (ret << 4) | v; + *data += 1; + } + return ret; +} + +// Parse a model like "xxyy00" +// and fill x, y and z values +// Little Endian encoding +// On exit, xyz is updated, and x_type, y_type, z_type contain the number of bytes read for each +void parseXYZ(const char *model, const SBuffer &payload, struct Z_XYZ_Var *xyz) { + const char *p = model; // pointer to the model character + uint32_t v = 0; // index in the payload bytes buffer + char c = pgm_read_byte(p); // cur char + while (c) { + char c1 = pgm_read_byte(p+1); // next char + if (!c1) { break; } // unexpected end of model + if (isXYZ(c) && (c == c1) && (v < payload.len())) { // if char is [x-z] and followed by same char + uint8_t val = payload.get8(v); + switch (c) { + case 'x': + xyz->x = xyz->x | (val << (xyz->x_type * 8)); + xyz->x_type++; + break; + case 'y': + xyz->y = xyz->y | (val << (xyz->y_type * 8)); + xyz->y_type++; + break; + case 'z': + xyz->z = xyz->z | (val << (xyz->z_type * 8)); + xyz->z_type++; + break; + } + } + p += 2; + v++; + c = pgm_read_byte(p); + } +} + +// works on big endiand hex only +// Returns if found: +// - cluster number +// - command number or 0xFF if command is part of the variable part +// - the payload in the form of a HEX string with x/y/z variables + + + +// Parse a cluster specific command, and try to convert into human readable +void convertClusterSpecific(JsonObject& json, uint16_t cluster, uint8_t cmd, bool direction, 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; + Z_XYZ_Var xyz; + +//AddLog_P2(LOG_LEVEL_INFO, PSTR(">>> len = %d - %02X%02X%02X"), payload.len(), payload.get8(0), payload.get8(1), payload.get8(2)); + for (uint32_t i = 0; i < sizeof(Z_Commands) / sizeof(Z_Commands[0]); i++) { + const Z_CommandConverter *conv = &Z_Commands[i]; + if (conv->cluster == cluster) { + // cluster match + if ((0xFF == conv->cmd) || (cmd == conv->cmd)) { + // cmd match + if ((direction && (conv->direction & 0x02)) || (!direction && (conv->direction & 0x01))) { + // check if we have a match for params too + // Match if: + // - payload exactly matches conv->param (conv->param may be longer) + // - payload matches conv->param until 'x', 'y' or 'z' + const char * p = conv->param; + //AddLog_P2(LOG_LEVEL_INFO, PSTR(">>>++1 param = %s"), p); + bool match = true; + for (uint8_t i = 0; i < payload.len(); i++) { + const char c1 = pgm_read_byte(p); + const char c2 = pgm_read_byte(p+1); + //AddLog_P2(LOG_LEVEL_INFO, PSTR(">>>++2 c1 = %c, c2 = %c"), c1, c2); + if ((0x00 == c1) || isXYZ(c1)) { + break; + } + const char * p2 = p; + uint32_t nextbyte = parseHex_P(&p2, 2); + //AddLog_P2(LOG_LEVEL_INFO, PSTR(">>>++3 parseHex_P = %02X"), nextbyte); + if (nextbyte != payload.get8(i)) { + match = false; + break; + } + p += 2; + } + if (match) { + command_name = (const __FlashStringHelper*) conv->tasmota_cmd; + parseXYZ(conv->param, payload, &xyz); + if (0xFF == conv->cmd) { + // shift all values + xyz.z = xyz.y; + xyz.z_type = xyz.y_type; + xyz.y = xyz.x; + xyz.y_type = xyz.x_type; + xyz.x = cmd; + xyz.x_type = 1; // 1 byte + } + break; + } + } + } + } + } + + // always report attribute in raw format + // Format: "0001!06": "00" = "!": "" for commands to devices + // 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); + + if (command_name) { + if (0 == xyz.x_type) { + json[command_name] = true; // no parameter + } else if (0 == xyz.y_type) { + json[command_name] = xyz.x; // 1 parameter + } else { + // multiple answers, create an array + JsonArray &arr = json.createNestedArray(command_name); + arr.add(xyz.x); + arr.add(xyz.y); + if (xyz.z_type) { + arr.add(xyz.z); + } + } + } +} + +// Find the command details by command name +// If not found: +// - returns nullptr +const __FlashStringHelper* zigbeeFindCommand(const char *command, uint16_t *cluster, uint16_t *cmd) { for (uint32_t i = 0; i < sizeof(Z_Commands) / sizeof(Z_Commands[0]); i++) { const Z_CommandConverter *conv = &Z_Commands[i]; if (0 == strcasecmp_P(command, conv->tasmota_cmd)) { - return (const __FlashStringHelper*) conv->zcl_cmd; + *cluster = conv->cluster; + *cmd = conv->cmd; + return (const __FlashStringHelper*) conv->param; } } return nullptr; } -inline bool isXYZ(char c) { - return (c >= 'x') && (c <= 'z'); -} - // take the lower 4 bits and turn it to an hex char inline char hexDigit(uint32_t h) { uint32_t nybble = h & 0x0F; diff --git a/tasmota/xdrv_23_zigbee_7_statemachine.ino b/tasmota/xdrv_23_zigbee_7_statemachine.ino index 5a3d78e85..d0616a40b 100644 --- a/tasmota/xdrv_23_zigbee_7_statemachine.ino +++ b/tasmota/xdrv_23_zigbee_7_statemachine.ino @@ -33,6 +33,7 @@ const uint8_t ZIGBEE_STATUS_NODE_DESC = 31; // Node descriptor const uint8_t ZIGBEE_STATUS_ACTIVE_EP = 32; // Endpoints descriptor const uint8_t ZIGBEE_STATUS_SIMPLE_DESC = 33; // Simple Descriptor (clusters) const uint8_t ZIGBEE_STATUS_DEVICE_INDICATION = 34; // Device announces its address +const uint8_t ZIGBEE_STATUS_DEVICE_IEEE = 35; // Request of device address const uint8_t ZIGBEE_STATUS_CC_VERSION = 50; // Status: CC2530 ZNP Version const uint8_t ZIGBEE_STATUS_CC_INFO = 51; // Status: CC2530 Device Configuration const uint8_t ZIGBEE_STATUS_UNSUPPORTED_VERSION = 98; // Unsupported ZNP version diff --git a/tasmota/xdrv_23_zigbee_8_parsers.ino b/tasmota/xdrv_23_zigbee_8_parsers.ino index 7d84124ed..b67add374 100644 --- a/tasmota/xdrv_23_zigbee_8_parsers.ino +++ b/tasmota/xdrv_23_zigbee_8_parsers.ino @@ -176,15 +176,24 @@ int32_t Z_ReceivePermitJoinStatus(int32_t res, const class SBuffer &buf) { return -1; } +// Send ZDO_IEEE_ADDR_REQ request to get IEEE long address +void Z_SendIEEEAddrReq(uint16_t shortaddr) { + uint8_t IEEEAddrReq[] = { Z_SREQ | Z_ZDO, ZDO_IEEE_ADDR_REQ, + Z_B0(shortaddr), Z_B1(shortaddr), 0x00, 0x00 }; + + ZigbeeZNPSend(IEEEAddrReq, sizeof(IEEEAddrReq)); +} + // Send ACTIVE_EP_REQ to collect active endpoints for this address void Z_SendActiveEpReq(uint16_t shortaddr) { uint8_t ActiveEpReq[] = { Z_SREQ | Z_ZDO, ZDO_ACTIVE_EP_REQ, Z_B0(shortaddr), Z_B1(shortaddr), Z_B0(shortaddr), Z_B1(shortaddr) }; - uint8_t NodeDescReq[] = { Z_SREQ | Z_ZDO, ZDO_NODE_DESC_REQ, - Z_B0(shortaddr), Z_B1(shortaddr), Z_B0(shortaddr), Z_B1(shortaddr) }; - ZigbeeZNPSend(ActiveEpReq, sizeof(ActiveEpReq)); + + // uint8_t NodeDescReq[] = { Z_SREQ | Z_ZDO, ZDO_NODE_DESC_REQ, + // Z_B0(shortaddr), Z_B1(shortaddr), Z_B0(shortaddr), Z_B1(shortaddr) }; + //ZigbeeZNPSend(NodeDescReq, sizeof(NodeDescReq)); Not sure this is useful } @@ -335,6 +344,40 @@ int32_t Z_ReceiveSimpleDesc(int32_t res, const class SBuffer &buf) { return -1; } +int32_t Z_ReceiveIEEEAddr(int32_t res, const class SBuffer &buf) { + uint8_t status = buf.get8(2); + Z_IEEEAddress ieeeAddr = buf.get64(3); + Z_ShortAddress nwkAddr = buf.get16(11); + // uint8_t startIndex = buf.get8(13); + // uint8_t numAssocDev = buf.get8(14); + + if (0 == status) { // SUCCESS + zigbee_devices.updateDevice(nwkAddr, ieeeAddr); + char hex[20]; + Uint64toHex(ieeeAddr, hex, 64); + Response_P(PSTR("{\"" D_JSON_ZIGBEE_STATE "\":{" + "\"Status\":%d,\"IEEEAddr\":\"%s\",\"ShortAddr\":\"0x%04X\"" + "}}"), + ZIGBEE_STATUS_DEVICE_IEEE, hex, nwkAddr + ); + + MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_ZIGBEEZCL_RECEIVED)); + XdrvRulesProcess(); + // Ping response + const String * friendlyName = zigbee_devices.getFriendlyName(nwkAddr); + if (friendlyName) { + Response_P(PSTR("{\"" D_JSON_ZIGBEE_PING "\":{\"" D_JSON_ZIGBEE_DEVICE "\":\"0x%04X\"" + ",\"" D_JSON_ZIGBEE_NAME "\":\"%s\"}}"), nwkAddr, friendlyName->c_str()); + } else { + Response_P(PSTR("{\"" D_JSON_ZIGBEE_PING "\":{\"" D_JSON_ZIGBEE_DEVICE "\":\"0x%04X\"}}"), nwkAddr); + } + + MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_ZIGBEEZCL_RECEIVED)); + XdrvRulesProcess(); + } + return -1; +} + int32_t Z_ReceiveEndDeviceAnnonce(int32_t res, const class SBuffer &buf) { Z_ShortAddress srcAddr = buf.get16(2); Z_ShortAddress nwkAddr = buf.get16(4); @@ -484,6 +527,7 @@ ZBM(AREQ_END_DEVICE_TC_DEV_IND, Z_AREQ | Z_ZDO, ZDO_TC_DEV_IND) // 45CA ZBM(AREQ_PERMITJOIN_OPEN_XX, Z_AREQ | Z_ZDO, ZDO_PERMIT_JOIN_IND ) // 45CB ZBM(AREQ_ZDO_ACTIVEEPRSP, Z_AREQ | Z_ZDO, ZDO_ACTIVE_EP_RSP) // 4585 ZBM(AREQ_ZDO_SIMPLEDESCRSP, Z_AREQ | Z_ZDO, ZDO_SIMPLE_DESC_RSP) // 4584 +ZBM(AREQ_ZDO_IEEE_ADDR_RSP, Z_AREQ | Z_ZDO, ZDO_IEEE_ADDR_RSP) // 4581 const Z_Dispatcher Z_DispatchTable[] PROGMEM = { { AREQ_AF_INCOMING_MESSAGE, &Z_ReceiveAfIncomingMessage }, @@ -493,6 +537,7 @@ const Z_Dispatcher Z_DispatchTable[] PROGMEM = { { AREQ_ZDO_NODEDESCRSP, &Z_ReceiveNodeDesc }, { AREQ_ZDO_ACTIVEEPRSP, &Z_ReceiveActiveEp }, { AREQ_ZDO_SIMPLEDESCRSP, &Z_ReceiveSimpleDesc }, + { AREQ_ZDO_IEEE_ADDR_RSP, &Z_ReceiveIEEEAddr }, }; int32_t Z_Recv_Default(int32_t res, const class SBuffer &buf) { diff --git a/tasmota/xdrv_23_zigbee_9_impl.ino b/tasmota/xdrv_23_zigbee_9_impl.ino index eccfc56be..08055ba9e 100644 --- a/tasmota/xdrv_23_zigbee_9_impl.ino +++ b/tasmota/xdrv_23_zigbee_9_impl.ino @@ -33,19 +33,22 @@ 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_FORGET "|" D_CMND_ZIGBEE_SAVE "|" D_CMND_ZIGBEE_NAME "|" D_CMND_ZIGBEE_BIND ; + D_CMND_ZIGBEE_FORGET "|" D_CMND_ZIGBEE_SAVE "|" D_CMND_ZIGBEE_NAME "|" D_CMND_ZIGBEE_BIND "|" + D_CMND_ZIGBEE_PING ; const char kZigbeeCommands[] PROGMEM = D_PRFX_ZIGBEE "|" // legacy prefix -- deprecated 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_FORGET "|" D_CMND_ZIGBEE_SAVE "|" D_CMND_ZIGBEE_NAME "|" D_CMND_ZIGBEE_BIND ; + D_CMND_ZIGBEE_FORGET "|" D_CMND_ZIGBEE_SAVE "|" D_CMND_ZIGBEE_NAME "|" D_CMND_ZIGBEE_BIND "|" + D_CMND_ZIGBEE_PING ; void (* const ZigbeeCommand[])(void) PROGMEM = { &CmndZbZNPSend, &CmndZbPermitJoin, &CmndZbStatus, &CmndZbReset, &CmndZbSend, &CmndZbProbe, &CmndZbRead, &CmndZbZNPReceive, - &CmndZbForget, &CmndZbSave, &CmndZbName, &CmndZbBind + &CmndZbForget, &CmndZbSave, &CmndZbName, &CmndZbBind, + &CmndZbPing, }; int32_t ZigbeeProcessInput(class SBuffer &buf) { @@ -367,61 +370,16 @@ void ZigbeeZCLSend(uint16_t dtsAddr, uint16_t clusterId, uint8_t endpoint, uint8 ZigbeeZNPSend(buf.getBuffer(), buf.len()); } -inline int8_t hexValue(char c) { - if ((c >= '0') && (c <= '9')) { - return c - '0'; - } - if ((c >= 'A') && (c <= 'F')) { - return 10 + c - 'A'; - } - if ((c >= 'a') && (c <= 'f')) { - return 10 + c - 'a'; - } - return -1; -} - -uint32_t parseHex(const char **data, size_t max_len = 8) { - uint32_t ret = 0; - for (uint32_t i = 0; i < max_len; i++) { - int8_t v = hexValue(**data); - if (v < 0) { break; } // non hex digit, we stop parsing - ret = (ret << 4) | v; - *data += 1; - } - return ret; -} - -void zigbeeZCLSendStr(uint16_t dstAddr, uint8_t endpoint, const char *data) { - - uint16_t cluster = 0x0000; // 0x0000 is a valid default value - uint8_t cmd = ZCL_READ_ATTRIBUTES; // default command is READ_ATTRIBUTES - bool clusterSpecific = false; - // 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 commanc - cluster = parseHex(&data, 4); - - // delimiter - if (('_' == *data) || ('!' == *data)) { - if ('!' == *data) { clusterSpecific = true; } - data++; - } else { - ResponseCmndChar("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 - - size_t size = strlen(data); +void zigbeeZCLSendStr(uint16_t dstAddr, uint8_t endpoint, bool clusterSpecific, + uint16_t cluster, uint8_t cmd, const char *param) { + size_t size = param ? strlen(param) : 0; SBuffer buf((size+2)/2); // actual bytes buffer for data - while (*data) { - uint8_t code = parseHex(&data, 2); - buf.add8(code); + if (param) { + while (*param) { + uint8_t code = parseHex_P(¶m, 2); + buf.add8(code); + } } if (0 == endpoint) { @@ -430,7 +388,7 @@ void zigbeeZCLSendStr(uint16_t dstAddr, uint8_t endpoint, const char *data) { AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZbSend: guessing endpoint 0x%02X"), endpoint); } AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZbSend: dstAddr 0x%04X, cluster 0x%04X, endpoint 0x%02X, cmd 0x%02X, data %s"), - dstAddr, cluster, endpoint, cmd, data); + dstAddr, cluster, endpoint, cmd, param); if (0 == endpoint) { AddLog_P2(LOG_LEVEL_INFO, PSTR("ZbSend: unspecified endpoint")); @@ -467,6 +425,9 @@ void CmndZbSend(void) { static char delim[] = ", "; // delimiters for parameters uint16_t device = 0xFFFF; // 0xFFFF is broadcast, so considered valid uint8_t endpoint = 0x00; // 0x00 is invalid for the dst endpoint + // Command elements + uint16_t cluster = 0; + uint8_t cmd = 0; String cmd_str = ""; // the actual low-level command, either specified or computed // parse JSON @@ -497,8 +458,9 @@ void CmndZbSend(void) { String key = it->key; JsonVariant& value = it->value; uint32_t x = 0, y = 0, z = 0; + uint16_t cmd_var; - const __FlashStringHelper* tasmota_cmd = zigbeeFindCommand(key.c_str()); + const __FlashStringHelper* tasmota_cmd = zigbeeFindCommand(key.c_str(), &cluster, &cmd_var); if (tasmota_cmd) { cmd_str = tasmota_cmd; } else { @@ -534,9 +496,16 @@ void CmndZbSend(void) { } } - AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZbSend: command_template = %s"), cmd_str.c_str()); + //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()); + //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZbSend: command_final = %s"), cmd_str.c_str()); } else { // we have zero command, pass through until last error for missing command } @@ -547,9 +516,9 @@ void CmndZbSend(void) { // we have an unsupported command type, just ignore it and fallback to missing command } - AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZbCmd_actual: ZigbeeZCLSend {\"device\":\"0x%04X\",\"endpoint\":%d,\"send\":\"%s\"}"), - device, endpoint, cmd_str.c_str()); - zigbeeZCLSendStr(device, endpoint, cmd_str.c_str()); + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZbCmd_actual: ZigbeeZCLSend {\"device\":\"0x%04X\",\"endpoint\":%d,\"send\":\"%04X!%02X/%s\"}"), + device, endpoint, cluster, cmd, cmd_str.c_str()); + zigbeeZCLSendStr(device, endpoint, true, cluster, cmd, cmd_str.c_str()); } else { Response_P(PSTR("Missing zigbee 'Send'")); return; @@ -615,16 +584,28 @@ void CmndZbBind(void) { // Probe a specific device to get its endpoints and supported clusters void CmndZbProbe(void) { + CmndZbProbeOrPing(true); +} + +void CmndZbProbeOrPing(boolean probe) { if (zigbee.init_phase) { ResponseCmndChar(D_ZIGBEE_NOT_STARTED); return; } uint16_t shortaddr = zigbee_devices.parseDeviceParam(XdrvMailbox.data); if (0x0000 == shortaddr) { ResponseCmndChar("Unknown device"); return; } if (0xFFFF == shortaddr) { ResponseCmndChar("Invalid parameter"); return; } // everything is good, we can send the command - Z_SendActiveEpReq(shortaddr); + Z_SendIEEEAddrReq(shortaddr); + if (probe) { + Z_SendActiveEpReq(shortaddr); + } ResponseCmndDone(); } +// Ping a device, actually a simplified version of ZbProbe +void CmndZbPing(void) { + CmndZbProbeOrPing(false); +} + // Specify, read or erase a Friendly Name void CmndZbName(void) { // Syntax is: @@ -729,6 +710,11 @@ void CmndZbRead(void) { } } + if (0 == endpoint) { // try to compute the endpoint + endpoint = zigbee_devices.findClusterEndpointIn(device, cluster); + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZbSend: guessing endpoint 0x%02X"), endpoint); + } + if ((0 != endpoint) && (attrs_len > 0)) { ZigbeeZCLSend(device, cluster, endpoint, ZCL_READ_ATTRIBUTES, false, attrs, attrs_len, true /* we do want a response */, zigbee_devices.getNextSeqNumber(device)); ResponseCmndDone();