diff --git a/tasmota/CHANGELOG.md b/tasmota/CHANGELOG.md index f850de769..2e9610d20 100644 --- a/tasmota/CHANGELOG.md +++ b/tasmota/CHANGELOG.md @@ -5,6 +5,7 @@ - Fix ESP32 PWM range - Add Zigbee better support for IKEA Motion Sensor - Add ESP32 Analog input support for GPIO32 to GPIO39 +- Add Zigbee add options to ``ZbSend`` ``Config`` and ``ReadCondig`` ### 8.4.0 20200730 diff --git a/tasmota/i18n.h b/tasmota/i18n.h index 6b97badd5..586bd470c 100644 --- a/tasmota/i18n.h +++ b/tasmota/i18n.h @@ -541,6 +541,8 @@ #define D_CMND_ZIGBEE_SEND "Send" #define D_CMND_ZIGBEE_WRITE "Write" #define D_CMND_ZIGBEE_REPORT "Report" +#define D_CMND_ZIGBEE_READ_CONFIG "ReadConfig" +#define D_CMND_ZIGBEE_CONFIG "Config" #define D_CMND_ZIGBEE_RESPONSE "Response" #define D_JSON_ZIGBEE_ZCL_SENT "ZbZCLSent" #define D_JSON_ZIGBEE_RECEIVED "ZbReceived" diff --git a/tasmota/xdrv_23_zigbee_5_converters.ino b/tasmota/xdrv_23_zigbee_5_converters.ino index aa9d7513f..d627c27a5 100644 --- a/tasmota/xdrv_23_zigbee_5_converters.ino +++ b/tasmota/xdrv_23_zigbee_5_converters.ino @@ -79,6 +79,16 @@ uint8_t Z_getDatatypeLen(uint8_t t) { } } +// is the type a discrete type, cf. section 2.6.2 of ZCL spec +bool Z_isDiscreteDataType(uint8_t t) { + if ( ((t >= 0x20) && (t <= 0x2F)) || // uint8 - int64 + ((t >= 0x38) && (t <= 0x3A)) || // semi - double + ((t >= 0xE0) && (t <= 0xE2)) ) { // ToD - UTC + return false; + } else { + return true; + } +} // return value: // 0 = keep initial value @@ -102,7 +112,7 @@ enum Cx_cluster_short { Cx0008, Cx0009, Cx000A, Cx000B, Cx000C, Cx000D, Cx000E, Cx000F, Cx0010, Cx0011, Cx0012, Cx0013, Cx0014, Cx001A, Cx0020, Cx0100, Cx0101, Cx0102, Cx0300, Cx0400, Cx0401, Cx0402, Cx0403, Cx0404, - Cx0405, Cx0406, Cx0B01, Cx0B05, + Cx0405, Cx0406, Cx0500, Cx0B01, Cx0B05, }; const uint16_t Cx_cluster[] PROGMEM = { @@ -110,7 +120,7 @@ const uint16_t Cx_cluster[] PROGMEM = { 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x001A, 0x0020, 0x0100, 0x0101, 0x0102, 0x0300, 0x0400, 0x0401, 0x0402, 0x0403, 0x0404, - 0x0405, 0x0406, 0x0B01, 0x0B05, + 0x0405, 0x0406, 0x0500, 0x0B01, 0x0B05, }; uint16_t CxToCluster(uint8_t cx) { @@ -216,6 +226,8 @@ ZF(Humidity) ZF(HumidityMinMeasuredValue) ZF(HumidityMaxMeasuredValue) ZF(Humidi ZF(Occupancy) ZF(OccupancySensorType) +ZF(ZoneState) ZF(ZoneType) ZF(ZoneStatus) + ZF(CompanyName) ZF(MeterTypeID) ZF(DataQualityID) ZF(CustomerName) ZF(Model) ZF(PartNumber) ZF(SoftwareRevision) ZF(POD) ZF(AvailablePower) ZF(PowerThreshold) ZF(ProductRevision) ZF(UtilityName) @@ -554,6 +566,11 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { { Zenum8, Cx0406, 0x0001, Z(OccupancySensorType), 1, Z_Nop }, // OccupancySensorType { Zunk, Cx0406, 0xFFFF, nullptr, 0, Z_Nop }, // Remove all other values + // IAS Cluster (Intruder Alarm System) + { Zenum8, Cx0500, 0x0000, Z(ZoneState), 1, Z_Nop }, // Occupancy (map8) + { Zenum16, Cx0500, 0x0001, Z(ZoneType), 1, Z_Nop }, // Occupancy (map8) + { Zmap16, Cx0500, 0x0002, Z(ZoneStatus), 1, Z_Nop }, // Occupancy (map8) + // Meter Identification cluster { Zstring, Cx0B01, 0x0000, Z(CompanyName), 1, Z_Nop }, { Zuint16, Cx0B01, 0x0001, Z(MeterTypeID), 1, Z_Nop }, @@ -589,6 +606,27 @@ typedef union ZCLHeaderFrameControl_t { } ZCLHeaderFrameControl_t; + +// Find the attribute details by attribute name +// If not found: +// - returns nullptr +const __FlashStringHelper* zigbeeFindAttributeByName(const char *command, + uint16_t *cluster, uint16_t *attribute, int16_t *multiplier, + uint8_t *cb) { + for (uint32_t i = 0; i < ARRAY_SIZE(Z_PostProcess); i++) { + const Z_AttributeConverter *converter = &Z_PostProcess[i]; + if (nullptr == converter->name) { continue; } // avoid strcasecmp_P() from crashing + if (0 == strcasecmp_P(command, converter->name)) { + if (cluster) { *cluster = CxToCluster(pgm_read_byte(&converter->cluster_short)); } + if (attribute) { *attribute = pgm_read_word(&converter->attribute); } + if (multiplier) { *multiplier = pgm_read_word(&converter->multiplier); } + if (cb) { *cb = pgm_read_byte(&converter->cb); } + return (const __FlashStringHelper*) converter->name; + } + } + return nullptr; +} + class ZCLFrame { public: @@ -660,6 +698,8 @@ public: void parseReportAttributes(JsonObject& json, uint8_t offset = 0); void parseReadAttributes(JsonObject& json, uint8_t offset = 0); void parseReadAttributesResponse(JsonObject& json, uint8_t offset = 0); + void parseReadConfigAttributes(JsonObject& json, uint8_t offset = 0); + void parseConfigAttributes(JsonObject& json, uint8_t offset = 0); void parseResponse(void); void parseClusterSpecificCommand(JsonObject& json, uint8_t offset = 0); void postProcessAttributes(uint16_t shortaddr, JsonObject& json); @@ -732,32 +772,13 @@ uint8_t toPercentageCR2032(uint32_t voltage) { // // 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 -// status: shall we insert a status OK (0x00) as required by ReadResponse -int32_t encodeSingleAttribute(class SBuffer &buf, const JsonVariant &val, float val_f, uint16_t attr, uint8_t attrtype, bool status = false) { +int32_t encodeSingleAttribute(class SBuffer &buf, double val_d, const char *val_str, uint8_t attrtype) { uint32_t len = Z_getDatatypeLen(attrtype); // pre-compute lenght, overloaded for variable length attributes - uint32_t u32; - int32_t i32; - float f32; - - if (&val) { - u32 = val.as(); - i32 = val.as(); - f32 = val.as(); - } else { - u32 = val_f; - i32 = val_f; - f32 = val_f; - } - - buf.add16(attr); // prepend with attribute identifier - if (status) { - buf.add8(Z_SUCCESS); // status OK = 0x00 - } - buf.add8(attrtype); // prepend with attribute type + uint32_t u32 = val_d; + int32_t i32 = val_d; + float f32 = val_d; switch (attrtype) { // unsigned 8 @@ -802,7 +823,6 @@ int32_t encodeSingleAttribute(class SBuffer &buf, const JsonVariant &val, float case Zstring: case Zstring16: { - const char * val_str = (&val) ? val.as() : ""; // avoid crash if &val is null if (nullptr == val_str) { return -2; } size_t val_len = strlen(val_str); if (val_len > 32) { val_len = 32; } @@ -819,18 +839,29 @@ int32_t encodeSingleAttribute(class SBuffer &buf, const JsonVariant &val, float break; default: - // remove the attribute type we just added - buf.setLen(buf.len() - (status ? 4 : 3)); return -1; } - return len + (status ? 4 : 3); + return len; } +// +// parse a single attribute +// +// Input: +// json: json Object where to add the attribute +// attrid_str: the key for the attribute +// buf: the buffer to read from +// offset: location in the buffer to read from +// attrtype: type of attribute (byte) or -1 to read from the stream as first byte +// Output: +// return: the length in bytes of the attribute uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer &buf, - uint32_t offset, uint32_t buflen) { + uint32_t offset, int32_t attrtype = -1) { uint32_t i = offset; - uint32_t attrtype = buf.get8(i++); + if (attrtype < 0) { + attrtype = buf.get8(i++); + } // fallback - enter a null value json[attrid_str] = (char*) nullptr; @@ -1061,7 +1092,7 @@ void ZCLFrame::parseReportAttributes(JsonObject& json, uint8_t offset) { _payload.set8(i, 0x41); // change type from 0x42 to 0x41 } } - i += parseSingleAttribute(json, key, _payload, i, len); + i += parseSingleAttribute(json, key, _payload, i); } } @@ -1075,7 +1106,7 @@ void ZCLFrame::parseReadAttributes(JsonObject& json, uint8_t offset) { JsonArray &attr_list = json.createNestedArray(F("Read")); JsonObject &attr_names = json.createNestedObject(F("ReadNames")); - while (len - i >= 2) { + while (len >= 2 + i) { uint16_t attrid = _payload.get16(i); attr_list.add(attrid); @@ -1094,6 +1125,79 @@ void ZCLFrame::parseReadAttributes(JsonObject& json, uint8_t offset) { } } +// ZCL_CONFIGURE_REPORTING_RESPONSE +void ZCLFrame::parseConfigAttributes(JsonObject& json, uint8_t offset) { + uint32_t i = offset; + uint32_t len = _payload.len(); + + JsonObject &config_rsp = json.createNestedObject(F("ConfigResponse")); + uint8_t status = _payload.get8(i); + config_rsp[F("Status")] = status; + config_rsp[F("StatusMsg")] = getZigbeeStatusMessage(status); +} + +// ZCL_READ_REPORTING_CONFIGURATION_RESPONSE +void ZCLFrame::parseReadConfigAttributes(JsonObject& json, uint8_t offset) { + uint32_t i = offset; + uint32_t len = _payload.len(); + + // json[F(D_CMND_ZIGBEE_CLUSTER)] = _cluster_id; // TODO is it necessary? + + JsonObject &attr_names = json.createNestedObject(F("ReadConfig")); + while (len >= i + 4) { + uint8_t status = _payload.get8(i); + uint8_t direction = _payload.get8(i+1); + uint16_t attrid = _payload.get16(i+2); + char attr_hex[12]; + snprintf_P(attr_hex, sizeof(attr_hex), "%04X/%04X", _cluster_id, attrid); + JsonObject &attr_details = attr_names.createNestedObject(attr_hex); + + if (direction) { + attr_details[F("DirectionReceived")] = true; + } + + // 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_details[(const __FlashStringHelper*) converter->name] = true; + break; + } + } + i += 4; + if (0 != status) { + attr_details[F("Status")] = status; + attr_details[F("StatusMsg")] = getZigbeeStatusMessage(status); + } else { + // no error, decode data + if (direction) { + // only Timeout period is present + uint16_t attr_timeout = _payload.get16(i); + i += 2; + attr_details[F("TimeoutPeriod")] = (0xFFFF == attr_timeout) ? -1 : attr_timeout; + } else { + // direction == 0, we have a data type + uint8_t attr_type = _payload.get8(i); + bool attr_discrete = Z_isDiscreteDataType(attr_type); + uint16_t attr_min_interval = _payload.get16(i+1); + uint16_t attr_max_interval = _payload.get16(i+3); + i += 5; + attr_details[F("MinInterval")] = (0xFFFF == attr_min_interval) ? -1 : attr_min_interval; + attr_details[F("MaxInterval")] = (0xFFFF == attr_max_interval) ? -1 : attr_max_interval; + if (!attr_discrete) { + // decode Reportable Change + char attr_name[20]; + strcpy_P(attr_name, PSTR("ReportableChange")); + i += parseSingleAttribute(attr_details, attr_name, _payload, i, attr_type); + } + } + } + } +} + // ZCL_READ_ATTRIBUTES_RESPONSE void ZCLFrame::parseReadAttributesResponse(JsonObject& json, uint8_t offset) { uint32_t i = offset; @@ -1108,7 +1212,7 @@ void ZCLFrame::parseReadAttributesResponse(JsonObject& json, uint8_t offset) { char key[16]; generateAttributeName(json, _cluster_id, attrid, key, sizeof(key)); - i += parseSingleAttribute(json, key, _payload, i, len); + i += parseSingleAttribute(json, key, _payload, i); } } } @@ -1321,10 +1425,10 @@ int32_t Z_AqaraSensorFunc(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObj const char * modelId_c = zigbee_devices.getModelId(shortaddr); // null if unknown String modelId((char*) modelId_c); - while (len - i >= 2) { + while (len >= 2 + i) { uint8_t attrid = buf2.get8(i++); - i += parseSingleAttribute(json, tmp, buf2, i, len); + i += parseSingleAttribute(json, tmp, buf2, i); float val = json[tmp]; json.remove(tmp); bool translated = false; // were we able to translate to a known format? @@ -1378,7 +1482,7 @@ int32_t Z_AqaraSensorFunc(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObj // apply the transformation from the converter int32_t Z_ApplyConverter(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, - uint16_t cluster, uint16_t attr, int16_t multiplier, uint16_t cb) { + uint16_t cluster, uint16_t attr, int16_t multiplier, uint8_t cb) { // apply multiplier if needed if (1 == multiplier) { // copy unchanged json[new_name] = value; @@ -1483,12 +1587,12 @@ void ZCLFrame::postProcessAttributes(uint16_t shortaddr, JsonObject& json) { } // Iterate on filter - for (uint32_t i = 0; i < sizeof(Z_PostProcess) / sizeof(Z_PostProcess[0]); i++) { + 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); int16_t conv_multiplier = pgm_read_word(&converter->multiplier); - uint16_t conv_cb = pgm_read_word(&converter->cb); // callback id + uint8_t conv_cb = pgm_read_byte(&converter->cb); // callback id if ((conv_cluster == cluster) && ((conv_attribute == attribute) || (conv_attribute == 0xFFFF)) ) { diff --git a/tasmota/xdrv_23_zigbee_6_commands.ino b/tasmota/xdrv_23_zigbee_6_commands.ino index c817dd0c2..c4e7e20c8 100644 --- a/tasmota/xdrv_23_zigbee_6_commands.ino +++ b/tasmota/xdrv_23_zigbee_6_commands.ino @@ -426,8 +426,12 @@ void convertClusterSpecific(JsonObject& json, uint16_t cluster, uint8_t cmd, boo if ((cluster == 0x0500) && (cmd == 0x00)) { // "ZoneStatusChange" json[command_name] = xyz.x; - json[command_name2 + F("Ext")] = xyz.y; - json[command_name2 + F("Zone")] = xyz.z; + if (0 != xyz.y) { + json[command_name2 + F("Ext")] = xyz.y; + } + if ((0 != xyz.z) && (0xFF != xyz.z)) { + json[command_name2 + F("Zone")] = xyz.z; + } } else if ((cluster == 0x0004) && ((cmd == 0x00) || (cmd == 0x01) || (cmd == 0x03))) { // AddGroupResp or ViewGroupResp (group name ignored) or RemoveGroup json[command_name] = xyz.y; @@ -525,6 +529,7 @@ void convertClusterSpecific(JsonObject& json, uint16_t cluster, uint8_t cmd, boo // If not found: // - returns nullptr const __FlashStringHelper* zigbeeFindCommand(const char *command, uint16_t *cluster, uint16_t *cmd) { + if (nullptr == command) { return nullptr; } for (uint32_t i = 0; i < sizeof(Z_Commands) / sizeof(Z_Commands[0]); i++) { const Z_CommandConverter *conv = &Z_Commands[i]; uint8_t conv_direction = pgm_read_byte(&conv->direction); diff --git a/tasmota/xdrv_23_zigbee_8_parsers.ino b/tasmota/xdrv_23_zigbee_8_parsers.ino index 6f702019f..f195fc7ac 100644 --- a/tasmota/xdrv_23_zigbee_8_parsers.ino +++ b/tasmota/xdrv_23_zigbee_8_parsers.ino @@ -1028,6 +1028,10 @@ void Z_IncomingMessage(ZCLFrame &zcl_received) { } else if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_READ_ATTRIBUTES == zcl_received.getCmdId())) { zcl_received.parseReadAttributes(json); // never defer read_attributes, so the auto-responder can send response back on a per cluster basis + } else if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_READ_REPORTING_CONFIGURATION_RESPONSE == zcl_received.getCmdId())) { + zcl_received.parseReadConfigAttributes(json); + } else if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_CONFIGURE_REPORTING_RESPONSE == zcl_received.getCmdId())) { + zcl_received.parseConfigAttributes(json); } else if (zcl_received.isClusterSpecificCommand()) { zcl_received.parseClusterSpecificCommand(json); } diff --git a/tasmota/xdrv_23_zigbee_A_impl.ino b/tasmota/xdrv_23_zigbee_A_impl.ino index 3332f9116..62451566c 100644 --- a/tasmota/xdrv_23_zigbee_A_impl.ino +++ b/tasmota/xdrv_23_zigbee_A_impl.ino @@ -185,7 +185,22 @@ void zigbeeZCLSendStr(uint16_t shortaddr, uint16_t groupaddr, uint8_t endpoint, } } -// Parse "Report", "Write" or "Response" attribute +// Special encoding for multiplier: +// multiplier == 0: ignore +// multiplier == 1: ignore +// multiplier > 0: divide by the multiplier +// multiplier < 0: multiply by the -multiplier (positive) +void ZbApplyMultiplier(double &val_d, int16_t multiplier) { + if ((0 != multiplier) && (1 != multiplier)) { + if (multiplier > 0) { // inverse of decoding + val_d = val_d / multiplier; + } else { + val_d = val_d * (-multiplier); + } + } +} + +// Parse "Report", "Write", "Response" or "Condig" attribute // Operation is one of: ZCL_REPORT_ATTRIBUTES (0x0A), ZCL_WRITE_ATTRIBUTES (0x02) or ZCL_READ_ATTRIBUTES_RESPONSE (0x01) void ZbSendReportWrite(const JsonObject &val_pubwrite, uint16_t device, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint16_t manuf, uint32_t operation) { SBuffer buf(200); // buffer to store the binary output of attibutes @@ -203,7 +218,8 @@ void ZbSendReportWrite(const JsonObject &val_pubwrite, uint16_t device, uint16_t uint16_t cluster_id = 0xFFFF; uint8_t type_id = Znodata; int16_t multiplier = 1; // multiplier to adjust the key value - float val_f = 0.0f; // alternative value if multiplier is used + double val_d = 0; // I try to avoid `double` but this type capture both float and (u)int32_t without prevision loss + const char* val_str = ""; // variant as string // 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) @@ -268,22 +284,88 @@ void ZbSendReportWrite(const JsonObject &val_pubwrite, uint16_t device, uint16_t ResponseCmndChar_P(PSTR("No more than one cluster id per command")); return; } - // apply multiplier if needed - bool use_val = true; - if ((0 != multiplier) && (1 != multiplier)) { - val_f = value; - if (multiplier > 0) { // inverse of decoding - val_f = val_f / multiplier; - } else { - val_f = val_f * (-multiplier); + + // //////////////////////////////////////////////////////////////////////////////// + // Split encoding depending on message + if (operation != ZCL_CONFIGURE_REPORTING) { + // apply multiplier if needed + val_d = value.as(); + val_str = value.as(); + ZbApplyMultiplier(val_d, multiplier); + + // push the value in the buffer + buf.add16(attr_id); // prepend with attribute identifier + if (operation == ZCL_READ_ATTRIBUTES_RESPONSE) { + buf.add8(Z_SUCCESS); // status OK = 0x00 + } + buf.add8(type_id); // prepend with attribute type + int32_t res = encodeSingleAttribute(buf, val_d, val_str, type_id); + if (res < 0) { + // remove the attribute type we just added + // buf.setLen(buf.len() - (operation == ZCL_READ_ATTRIBUTES_RESPONSE ? 4 : 3)); + Response_P(PSTR("{\"%s\":\"%s'%s' 0x%02X\"}"), XdrvMailbox.command, PSTR("Unsupported attribute type "), key, type_id); + return; + } + + } else { + // //////////////////////////////////////////////////////////////////////////////// + // ZCL_CONFIGURE_REPORTING + if (!value.is()) { + ResponseCmndChar_P(PSTR("Config requires JSON objects")); + return; + } + JsonObject &attr_config = value.as(); + bool attr_direction = false; + + const JsonVariant &val_attr_direction = GetCaseInsensitive(attr_config, PSTR("DirectionReceived")); + if (nullptr != &val_attr_direction) { + uint32_t dir = strToUInt(val_attr_direction); + if (dir) { + attr_direction = true; + } + } + + // read MinInterval and MaxInterval, default to 0xFFFF if not specified + uint16_t attr_min_interval = 0xFFFF; + uint16_t attr_max_interval = 0xFFFF; + const JsonVariant &val_attr_min = GetCaseInsensitive(attr_config, PSTR("MinInterval")); + if (nullptr != &val_attr_min) { attr_min_interval = strToUInt(val_attr_min); } + const JsonVariant &val_attr_max = GetCaseInsensitive(attr_config, PSTR("MaxInterval")); + if (nullptr != &val_attr_max) { attr_max_interval = strToUInt(val_attr_max); } + + // read ReportableChange + const JsonVariant &val_attr_rc = GetCaseInsensitive(attr_config, PSTR("ReportableChange")); + if (nullptr != &val_attr_rc) { + val_d = val_attr_rc.as(); + val_str = val_attr_rc.as(); + ZbApplyMultiplier(val_d, multiplier); + } + + // read TimeoutPeriod + uint16_t attr_timeout = 0x0000; + const JsonVariant &val_attr_timeout = GetCaseInsensitive(attr_config, PSTR("TimeoutPeriod")); + if (nullptr != &val_attr_timeout) { attr_timeout = strToUInt(val_attr_timeout); } + + bool attr_discrete = Z_isDiscreteDataType(type_id); + + // all fields are gathered, output the butes into the buffer, ZCL 2.5.7.1 + // common bytes + buf.add8(attr_direction ? 0x01 : 0x00); + buf.add16(attr_id); + if (attr_direction) { + buf.add16(attr_timeout); + } else { + buf.add8(type_id); + buf.add16(attr_min_interval); + buf.add16(attr_max_interval); + if (!attr_discrete) { + int32_t res = encodeSingleAttribute(buf, val_d, val_str, type_id); + if (res < 0) { + Response_P(PSTR("{\"%s\":\"%s'%s' 0x%02X\"}"), XdrvMailbox.command, PSTR("Unsupported attribute type "), key, type_id); + return; + } + } } - use_val = false; - } - // push the value in the buffer - int32_t res = encodeSingleAttribute(buf, use_val ? value : *(const JsonVariant*)nullptr, val_f, attr_id, type_id, operation == ZCL_READ_ATTRIBUTES_RESPONSE); // force status if Reponse - if (res < 0) { - Response_P(PSTR("{\"%s\":\"%s'%s' 0x%02X\"}"), XdrvMailbox.command, PSTR("Unsupported attribute type "), key, type_id); - return; } } @@ -428,39 +510,50 @@ void ZbSendSend(const JsonVariant &val_cmd, uint16_t device, uint16_t groupaddr, // 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) { +void ZbSendRead(const JsonVariant &val_attr, uint16_t device, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint16_t manuf, uint32_t operation) { // 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}} + // ZbSend {"Device":"0xF289","ReadConig":{"Power":true}} + // ZbSend {"Device":"0xF289","Cluster":6,"Endpoint":3,"ReadConfig":0} + // params size_t attrs_len = 0; uint8_t* attrs = nullptr; // empty string is valid + size_t attr_item_len = 2; // how many bytes per attribute, standard for "Read" + size_t attr_item_offset = 0; // how many bytes do we offset to store attribute + if (ZCL_READ_REPORTING_CONFIGURATION == operation) { + attr_item_len = 3; + attr_item_offset = 1; + } 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]; + attrs_len = attr_arr.size() * attr_item_len; + attrs = (uint8_t*) calloc(attrs_len, 1); uint32_t i = 0; for (auto value : attr_arr) { uint16_t val = strToUInt(value); + i += attr_item_offset; attrs[i++] = val & 0xFF; attrs[i++] = val >> 8; + i += attr_item_len - 2 - attr_item_offset; // normally 0 } } else if (val_attr.is()) { const JsonObject& attr_obj = val_attr.as(); - attrs_len = attr_obj.size() * 2; - attrs = new uint8_t[attrs_len]; + attrs_len = attr_obj.size() * attr_item_len; + attrs = (uint8_t*) calloc(attrs_len, 1); 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 + 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 @@ -475,15 +568,21 @@ void ZbSendRead(const JsonVariant &val_attr, uint16_t device, uint16_t groupaddr // match name // check if there is a conflict with cluster // TODO + if (!value && attr_item_offset) { + // If value is false (non-default) then set direction to 1 (for ReadConfig) + attrs[actual_attr_len] = 0x01; + } + actual_attr_len += attr_item_offset; attrs[actual_attr_len++] = local_attr_id & 0xFF; attrs[actual_attr_len++] = local_attr_id >> 8; + actual_attr_len += attr_item_len - 2 - attr_item_offset; // normally 0 found = true; // check cluster if (0xFFFF == cluster) { cluster = local_cluster_id; } else if (cluster != local_cluster_id) { ResponseCmndChar_P(PSTR("No more than one cluster id per command")); - if (attrs) { delete[] attrs; } + if (attrs) { free(attrs); } return; } break; // found, exit loop @@ -496,20 +595,20 @@ void ZbSendRead(const JsonVariant &val_attr, uint16_t device, uint16_t groupaddr 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; + attrs_len = attr_item_len; + attrs = (uint8_t*) calloc(attrs_len, 1); + attrs[0 + attr_item_offset] = val & 0xFF; // little endian + attrs[1 + attr_item_offset] = 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)); + ZigbeeZCLSend_Raw(device, groupaddr, cluster, endpoint, operation, 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; } + if (attrs) { free(attrs); } } // @@ -594,9 +693,12 @@ void CmndZbSend(void) { const JsonVariant &val_write = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_WRITE)); const JsonVariant &val_publish = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_REPORT)); const JsonVariant &val_response = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_RESPONSE)); - uint32_t multi_cmd = (nullptr != &val_cmd) + (nullptr != &val_read) + (nullptr != &val_write) + (nullptr != &val_publish)+ (nullptr != &val_response); + const JsonVariant &val_read_config = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_READ_CONFIG)); + const JsonVariant &val_config = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_CONFIG)); + uint32_t multi_cmd = (nullptr != &val_cmd) + (nullptr != &val_read) + (nullptr != &val_write) + (nullptr != &val_publish) + + (nullptr != &val_response) + (nullptr != &val_read_config) + (nullptr != &val_config); if (multi_cmd > 1) { - ResponseCmndChar_P(PSTR("Can only have one of: 'Send', 'Read', 'Write', 'Report' or 'Reponse'")); + ResponseCmndChar_P(PSTR("Can only have one of: 'Send', 'Read', 'Write', 'Report', 'Reponse', 'ReadConfig' or 'Config'")); return; } // from here we have one and only one command @@ -608,7 +710,7 @@ void CmndZbSend(void) { } else if (nullptr != &val_read) { // "Read":{...attributes...}, "Read":attribute or "Read":[...attributes...] // we accept eitehr a number, a string, an array of numbers/strings, or a JSON object - ZbSendRead(val_read, device, groupaddr, cluster, endpoint, manuf); + ZbSendRead(val_read, device, groupaddr, cluster, endpoint, manuf, ZCL_READ_ATTRIBUTES); } else if (nullptr != &val_write) { // only KSON object if (!val_write.is()) { @@ -618,7 +720,7 @@ void CmndZbSend(void) { // "Write":{...attributes...} ZbSendReportWrite(val_write, device, groupaddr, cluster, endpoint, manuf, ZCL_WRITE_ATTRIBUTES); } else if (nullptr != &val_publish) { - // "Report":{...attributes...} + // "Publish":{...attributes...} // only KSON object if (!val_publish.is()) { ResponseCmndChar_P(PSTR("Missing parameters")); @@ -633,6 +735,18 @@ void CmndZbSend(void) { return; } ZbSendReportWrite(val_response, device, groupaddr, cluster, endpoint, manuf, ZCL_READ_ATTRIBUTES_RESPONSE); + } else if (nullptr != &val_read_config) { + // "ReadConfg":{...attributes...}, "ReadConfg":attribute or "ReadConfg":[...attributes...] + // we accept eitehr a number, a string, an array of numbers/strings, or a JSON object + ZbSendRead(val_read_config, device, groupaddr, cluster, endpoint, manuf, ZCL_READ_REPORTING_CONFIGURATION); + } else if (nullptr != &val_config) { + // "Config":{...attributes...} + // only JSON object + if (!val_config.is()) { + ResponseCmndChar_P(PSTR("Missing parameters")); + return; + } + ZbSendReportWrite(val_config, device, groupaddr, cluster, endpoint, manuf, ZCL_CONFIGURE_REPORTING); } else { Response_P(PSTR("Missing zigbee 'Send', 'Write', 'Report' or 'Response'")); return; @@ -675,18 +789,36 @@ void ZbBindUnbind(bool unbind) { // false = bind, true = unbind // look for source endpoint const JsonVariant &val_endpoint = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_ENDPOINT)); if (nullptr != &val_endpoint) { endpoint = strToUInt(val_endpoint); } + else { endpoint = zigbee_devices.findFirstEndpoint(srcDevice); } // look for source cluster const JsonVariant &val_cluster = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_CLUSTER)); - if (nullptr != &val_cluster) { cluster = strToUInt(val_cluster); } + if (nullptr != &val_cluster) { + cluster = strToUInt(val_cluster); // first convert as number + if (0 == cluster) { + zigbeeFindAttributeByName(val_cluster.as(), &cluster, nullptr, nullptr, nullptr); + } + } + + // Or Group Address - we don't need a dstEndpoint in this case + const JsonVariant &to_group = GetCaseInsensitive(json, PSTR("ToGroup")); + if (nullptr != &to_group) { toGroup = strToUInt(to_group); } // Either Device address // In this case the following parameters are mandatory // - "ToDevice" and the device must have a known IEEE address // - "ToEndpoint" const JsonVariant &dst_device = GetCaseInsensitive(json, PSTR("ToDevice")); - if (nullptr != &dst_device) { - dstDevice = zigbee_devices.parseDeviceParam(dst_device.as()); - if (BAD_SHORTADDR == dstDevice) { ResponseCmndChar_P(PSTR("Invalid parameter")); return; } + + // If no target is specified, we default to coordinator 0x0000 + if ((nullptr == &to_group) && (nullptr == &dst_device)) { + dstDevice = 0x0000; + } + + if ((nullptr != &dst_device) || (BAD_SHORTADDR != dstDevice)) { + if (BAD_SHORTADDR == dstDevice) { + dstDevice = zigbee_devices.parseDeviceParam(dst_device.as()); + if (BAD_SHORTADDR == dstDevice) { ResponseCmndChar_P(PSTR("Invalid parameter")); return; } + } if (0x0000 == dstDevice) { dstLongAddr = localIEEEAddr; } else { @@ -695,13 +827,10 @@ void ZbBindUnbind(bool unbind) { // false = bind, true = unbind if (0 == dstLongAddr) { ResponseCmndChar_P(PSTR("Unknown dest IEEE address")); return; } const JsonVariant &val_toendpoint = GetCaseInsensitive(json, PSTR("ToEndpoint")); - if (nullptr != &val_toendpoint) { toendpoint = strToUInt(val_toendpoint); } else { toendpoint = endpoint; } + if (nullptr != &val_toendpoint) { toendpoint = strToUInt(val_toendpoint); } + else { toendpoint = 0x01; } // default to endpoint 1 } - // Or Group Address - we don't need a dstEndpoint in this case - const JsonVariant &to_group = GetCaseInsensitive(json, PSTR("ToGroup")); - if (nullptr != &to_group) { toGroup = strToUInt(to_group); } - // make sure we don't have conflicting parameters if (&to_group && dstLongAddr) { ResponseCmndChar_P(PSTR("Cannot have both \"ToDevice\" and \"ToGroup\"")); return; } if (!&to_group && !dstLongAddr) { ResponseCmndChar_P(PSTR("Missing \"ToDevice\" or \"ToGroup\"")); return; }