Add Zigbee add options to ``ZbSend`` ``Config`` and ``ReadCondig``

This allows to configure the attribute reporting of devices
This commit is contained in:
Stephan Hadinger 2020-08-08 12:17:37 +02:00
parent 5e064c1531
commit bf1d76e28f
6 changed files with 329 additions and 84 deletions

View File

@ -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

View File

@ -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"

View File

@ -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<uint32_t>();
i32 = val.as<int32_t>();
f32 = val.as<float>();
} 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<const char*>() : ""; // 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)) ) {

View File

@ -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);

View File

@ -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);
}

View File

@ -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<double>();
val_str = value.as<const char*>();
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<JsonObject>()) {
ResponseCmndChar_P(PSTR("Config requires JSON objects"));
return;
}
JsonObject &attr_config = value.as<JsonObject>();
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<double>();
val_str = val_attr_rc.as<const char*>();
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<JsonArray>()) {
const JsonArray& attr_arr = val_attr.as<const JsonArray&>();
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<JsonObject>()) {
const JsonObject& attr_obj = val_attr.as<const JsonObject&>();
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<JsonObject>()) {
@ -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<JsonObject>()) {
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<JsonObject>()) {
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<const char*>(), &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<char*>());
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<char*>());
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; }