Merge pull request #9050 from s-hadinger/zigbee_ezsp_aug_8

Add Zigbee add options to ``ZbSend`` ``Config`` and ``ReadCondig``
This commit is contained in:
Theo Arends 2020-08-08 12:56:56 +02:00 committed by GitHub
commit 51a70cb8f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 329 additions and 84 deletions

View File

@ -5,6 +5,7 @@
- Fix ESP32 PWM range - Fix ESP32 PWM range
- Add Zigbee better support for IKEA Motion Sensor - Add Zigbee better support for IKEA Motion Sensor
- Add ESP32 Analog input support for GPIO32 to GPIO39 - Add ESP32 Analog input support for GPIO32 to GPIO39
- Add Zigbee add options to ``ZbSend`` ``Config`` and ``ReadCondig``
### 8.4.0 20200730 ### 8.4.0 20200730

View File

@ -541,6 +541,8 @@
#define D_CMND_ZIGBEE_SEND "Send" #define D_CMND_ZIGBEE_SEND "Send"
#define D_CMND_ZIGBEE_WRITE "Write" #define D_CMND_ZIGBEE_WRITE "Write"
#define D_CMND_ZIGBEE_REPORT "Report" #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_CMND_ZIGBEE_RESPONSE "Response"
#define D_JSON_ZIGBEE_ZCL_SENT "ZbZCLSent" #define D_JSON_ZIGBEE_ZCL_SENT "ZbZCLSent"
#define D_JSON_ZIGBEE_RECEIVED "ZbReceived" #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: // return value:
// 0 = keep initial value // 0 = keep initial value
@ -102,7 +112,7 @@ enum Cx_cluster_short {
Cx0008, Cx0009, Cx000A, Cx000B, Cx000C, Cx000D, Cx000E, Cx000F, Cx0008, Cx0009, Cx000A, Cx000B, Cx000C, Cx000D, Cx000E, Cx000F,
Cx0010, Cx0011, Cx0012, Cx0013, Cx0014, Cx001A, Cx0020, Cx0100, Cx0010, Cx0011, Cx0012, Cx0013, Cx0014, Cx001A, Cx0020, Cx0100,
Cx0101, Cx0102, Cx0300, Cx0400, Cx0401, Cx0402, Cx0403, Cx0404, Cx0101, Cx0102, Cx0300, Cx0400, Cx0401, Cx0402, Cx0403, Cx0404,
Cx0405, Cx0406, Cx0B01, Cx0B05, Cx0405, Cx0406, Cx0500, Cx0B01, Cx0B05,
}; };
const uint16_t Cx_cluster[] PROGMEM = { const uint16_t Cx_cluster[] PROGMEM = {
@ -110,7 +120,7 @@ const uint16_t Cx_cluster[] PROGMEM = {
0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x001A, 0x0020, 0x0100, 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x001A, 0x0020, 0x0100,
0x0101, 0x0102, 0x0300, 0x0400, 0x0401, 0x0402, 0x0403, 0x0404, 0x0101, 0x0102, 0x0300, 0x0400, 0x0401, 0x0402, 0x0403, 0x0404,
0x0405, 0x0406, 0x0B01, 0x0B05, 0x0405, 0x0406, 0x0500, 0x0B01, 0x0B05,
}; };
uint16_t CxToCluster(uint8_t cx) { uint16_t CxToCluster(uint8_t cx) {
@ -216,6 +226,8 @@ ZF(Humidity) ZF(HumidityMinMeasuredValue) ZF(HumidityMaxMeasuredValue) ZF(Humidi
ZF(Occupancy) ZF(OccupancySensorType) ZF(Occupancy) ZF(OccupancySensorType)
ZF(ZoneState) ZF(ZoneType) ZF(ZoneStatus)
ZF(CompanyName) ZF(MeterTypeID) ZF(DataQualityID) ZF(CustomerName) ZF(Model) ZF(PartNumber) ZF(CompanyName) ZF(MeterTypeID) ZF(DataQualityID) ZF(CustomerName) ZF(Model) ZF(PartNumber)
ZF(SoftwareRevision) ZF(POD) ZF(AvailablePower) ZF(PowerThreshold) ZF(ProductRevision) ZF(UtilityName) 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 { Zenum8, Cx0406, 0x0001, Z(OccupancySensorType), 1, Z_Nop }, // OccupancySensorType
{ Zunk, Cx0406, 0xFFFF, nullptr, 0, Z_Nop }, // Remove all other values { 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 // Meter Identification cluster
{ Zstring, Cx0B01, 0x0000, Z(CompanyName), 1, Z_Nop }, { Zstring, Cx0B01, 0x0000, Z(CompanyName), 1, Z_Nop },
{ Zuint16, Cx0B01, 0x0001, Z(MeterTypeID), 1, Z_Nop }, { Zuint16, Cx0B01, 0x0001, Z(MeterTypeID), 1, Z_Nop },
@ -589,6 +606,27 @@ typedef union ZCLHeaderFrameControl_t {
} 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 { class ZCLFrame {
public: public:
@ -660,6 +698,8 @@ public:
void parseReportAttributes(JsonObject& json, uint8_t offset = 0); void parseReportAttributes(JsonObject& json, uint8_t offset = 0);
void parseReadAttributes(JsonObject& json, uint8_t offset = 0); void parseReadAttributes(JsonObject& json, uint8_t offset = 0);
void parseReadAttributesResponse(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 parseResponse(void);
void parseClusterSpecificCommand(JsonObject& json, uint8_t offset = 0); void parseClusterSpecificCommand(JsonObject& json, uint8_t offset = 0);
void postProcessAttributes(uint16_t shortaddr, JsonObject& json); 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 // Appends the attribute value to Write or to Report
// Adds to buf: // Adds to buf:
// - 2 bytes: attribute identigier
// - 1 byte: attribute type
// - n bytes: value (typically between 1 and 4 bytes, or bigger for strings) // - n bytes: value (typically between 1 and 4 bytes, or bigger for strings)
// returns number of bytes of attribute, or <0 if error // 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, double val_d, const char *val_str, uint8_t attrtype) {
int32_t encodeSingleAttribute(class SBuffer &buf, const JsonVariant &val, float val_f, uint16_t attr, uint8_t attrtype, bool status = false) {
uint32_t len = Z_getDatatypeLen(attrtype); // pre-compute lenght, overloaded for variable length attributes uint32_t len = Z_getDatatypeLen(attrtype); // pre-compute lenght, overloaded for variable length attributes
uint32_t u32; uint32_t u32 = val_d;
int32_t i32; int32_t i32 = val_d;
float f32; float f32 = val_d;
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
switch (attrtype) { switch (attrtype) {
// unsigned 8 // unsigned 8
@ -802,7 +823,6 @@ int32_t encodeSingleAttribute(class SBuffer &buf, const JsonVariant &val, float
case Zstring: case Zstring:
case Zstring16: case Zstring16:
{ {
const char * val_str = (&val) ? val.as<const char*>() : ""; // avoid crash if &val is null
if (nullptr == val_str) { return -2; } if (nullptr == val_str) { return -2; }
size_t val_len = strlen(val_str); size_t val_len = strlen(val_str);
if (val_len > 32) { val_len = 32; } if (val_len > 32) { val_len = 32; }
@ -819,18 +839,29 @@ int32_t encodeSingleAttribute(class SBuffer &buf, const JsonVariant &val, float
break; break;
default: default:
// remove the attribute type we just added
buf.setLen(buf.len() - (status ? 4 : 3));
return -1; 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 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 i = offset;
uint32_t attrtype = buf.get8(i++); if (attrtype < 0) {
attrtype = buf.get8(i++);
}
// fallback - enter a null value // fallback - enter a null value
json[attrid_str] = (char*) nullptr; 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 _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")); JsonArray &attr_list = json.createNestedArray(F("Read"));
JsonObject &attr_names = json.createNestedObject(F("ReadNames")); JsonObject &attr_names = json.createNestedObject(F("ReadNames"));
while (len - i >= 2) { while (len >= 2 + i) {
uint16_t attrid = _payload.get16(i); uint16_t attrid = _payload.get16(i);
attr_list.add(attrid); 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 // ZCL_READ_ATTRIBUTES_RESPONSE
void ZCLFrame::parseReadAttributesResponse(JsonObject& json, uint8_t offset) { void ZCLFrame::parseReadAttributesResponse(JsonObject& json, uint8_t offset) {
uint32_t i = offset; uint32_t i = offset;
@ -1108,7 +1212,7 @@ void ZCLFrame::parseReadAttributesResponse(JsonObject& json, uint8_t offset) {
char key[16]; char key[16];
generateAttributeName(json, _cluster_id, attrid, key, sizeof(key)); 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 const char * modelId_c = zigbee_devices.getModelId(shortaddr); // null if unknown
String modelId((char*) modelId_c); String modelId((char*) modelId_c);
while (len - i >= 2) { while (len >= 2 + i) {
uint8_t attrid = buf2.get8(i++); uint8_t attrid = buf2.get8(i++);
i += parseSingleAttribute(json, tmp, buf2, i, len); i += parseSingleAttribute(json, tmp, buf2, i);
float val = json[tmp]; float val = json[tmp];
json.remove(tmp); json.remove(tmp);
bool translated = false; // were we able to translate to a known format? 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 // 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, 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 // apply multiplier if needed
if (1 == multiplier) { // copy unchanged if (1 == multiplier) { // copy unchanged
json[new_name] = value; json[new_name] = value;
@ -1483,12 +1587,12 @@ void ZCLFrame::postProcessAttributes(uint16_t shortaddr, JsonObject& json) {
} }
// Iterate on filter // 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]; const Z_AttributeConverter *converter = &Z_PostProcess[i];
uint16_t conv_cluster = CxToCluster(pgm_read_byte(&converter->cluster_short)); uint16_t conv_cluster = CxToCluster(pgm_read_byte(&converter->cluster_short));
uint16_t conv_attribute = pgm_read_word(&converter->attribute); uint16_t conv_attribute = pgm_read_word(&converter->attribute);
int16_t conv_multiplier = pgm_read_word(&converter->multiplier); 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) && if ((conv_cluster == cluster) &&
((conv_attribute == attribute) || (conv_attribute == 0xFFFF)) ) { ((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)) { if ((cluster == 0x0500) && (cmd == 0x00)) {
// "ZoneStatusChange" // "ZoneStatusChange"
json[command_name] = xyz.x; json[command_name] = xyz.x;
json[command_name2 + F("Ext")] = xyz.y; if (0 != xyz.y) {
json[command_name2 + F("Zone")] = xyz.z; 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))) { } else if ((cluster == 0x0004) && ((cmd == 0x00) || (cmd == 0x01) || (cmd == 0x03))) {
// AddGroupResp or ViewGroupResp (group name ignored) or RemoveGroup // AddGroupResp or ViewGroupResp (group name ignored) or RemoveGroup
json[command_name] = xyz.y; json[command_name] = xyz.y;
@ -525,6 +529,7 @@ void convertClusterSpecific(JsonObject& json, uint16_t cluster, uint8_t cmd, boo
// If not found: // If not found:
// - returns nullptr // - returns nullptr
const __FlashStringHelper* zigbeeFindCommand(const char *command, uint16_t *cluster, uint16_t *cmd) { 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++) { for (uint32_t i = 0; i < sizeof(Z_Commands) / sizeof(Z_Commands[0]); i++) {
const Z_CommandConverter *conv = &Z_Commands[i]; const Z_CommandConverter *conv = &Z_Commands[i];
uint8_t conv_direction = pgm_read_byte(&conv->direction); 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())) { } else if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_READ_ATTRIBUTES == zcl_received.getCmdId())) {
zcl_received.parseReadAttributes(json); zcl_received.parseReadAttributes(json);
// never defer read_attributes, so the auto-responder can send response back on a per cluster basis // 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()) { } else if (zcl_received.isClusterSpecificCommand()) {
zcl_received.parseClusterSpecificCommand(json); 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) // 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) { 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 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; uint16_t cluster_id = 0xFFFF;
uint8_t type_id = Znodata; uint8_t type_id = Znodata;
int16_t multiplier = 1; // multiplier to adjust the key value 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 // 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) // 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")); ResponseCmndChar_P(PSTR("No more than one cluster id per command"));
return; return;
} }
// apply multiplier if needed
bool use_val = true; // ////////////////////////////////////////////////////////////////////////////////
if ((0 != multiplier) && (1 != multiplier)) { // Split encoding depending on message
val_f = value; if (operation != ZCL_CONFIGURE_REPORTING) {
if (multiplier > 0) { // inverse of decoding // apply multiplier if needed
val_f = val_f / multiplier; val_d = value.as<double>();
} else { val_str = value.as<const char*>();
val_f = val_f * (-multiplier); 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 // 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":0,"Endpoint":3,"Read":5}
// ZbSend {"Device":"0xF289","Cluster":"0x0000","Endpoint":"0x0003","Read":"0x0005"} // ZbSend {"Device":"0xF289","Cluster":"0x0000","Endpoint":"0x0003","Read":"0x0005"}
// ZbSend {"Device":"0xF289","Cluster":0,"Endpoint":3,"Read":[5,6,7,4]} // ZbSend {"Device":"0xF289","Cluster":0,"Endpoint":3,"Read":[5,6,7,4]}
// ZbSend {"Device":"0xF289","Endpoint":3,"Read":{"ModelId":true}} // ZbSend {"Device":"0xF289","Endpoint":3,"Read":{"ModelId":true}}
// ZbSend {"Device":"0xF289","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 // params
size_t attrs_len = 0; size_t attrs_len = 0;
uint8_t* attrs = nullptr; // empty string is valid 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); uint16_t val = strToUInt(val_attr);
if (val_attr.is<JsonArray>()) { if (val_attr.is<JsonArray>()) {
const JsonArray& attr_arr = val_attr.as<const JsonArray&>(); const JsonArray& attr_arr = val_attr.as<const JsonArray&>();
attrs_len = attr_arr.size() * 2; attrs_len = attr_arr.size() * attr_item_len;
attrs = new uint8_t[attrs_len]; attrs = (uint8_t*) calloc(attrs_len, 1);
uint32_t i = 0; uint32_t i = 0;
for (auto value : attr_arr) { for (auto value : attr_arr) {
uint16_t val = strToUInt(value); uint16_t val = strToUInt(value);
i += attr_item_offset;
attrs[i++] = val & 0xFF; attrs[i++] = val & 0xFF;
attrs[i++] = val >> 8; attrs[i++] = val >> 8;
i += attr_item_len - 2 - attr_item_offset; // normally 0
} }
} else if (val_attr.is<JsonObject>()) { } else if (val_attr.is<JsonObject>()) {
const JsonObject& attr_obj = val_attr.as<const JsonObject&>(); const JsonObject& attr_obj = val_attr.as<const JsonObject&>();
attrs_len = attr_obj.size() * 2; attrs_len = attr_obj.size() * attr_item_len;
attrs = new uint8_t[attrs_len]; attrs = (uint8_t*) calloc(attrs_len, 1);
uint32_t actual_attr_len = 0; uint32_t actual_attr_len = 0;
// iterate on keys // iterate on keys
for (JsonObject::const_iterator it=attr_obj.begin(); it!=attr_obj.end(); ++it) { for (JsonObject::const_iterator it=attr_obj.begin(); it!=attr_obj.end(); ++it) {
const char *key = it->key; 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; bool found = false;
// scan attributes to find by name, and retrieve type // 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 // match name
// check if there is a conflict with cluster // check if there is a conflict with cluster
// TODO // 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 & 0xFF;
attrs[actual_attr_len++] = local_attr_id >> 8; attrs[actual_attr_len++] = local_attr_id >> 8;
actual_attr_len += attr_item_len - 2 - attr_item_offset; // normally 0
found = true; found = true;
// check cluster // check cluster
if (0xFFFF == cluster) { if (0xFFFF == cluster) {
cluster = local_cluster_id; cluster = local_cluster_id;
} else if (cluster != local_cluster_id) { } else if (cluster != local_cluster_id) {
ResponseCmndChar_P(PSTR("No more than one cluster id per command")); ResponseCmndChar_P(PSTR("No more than one cluster id per command"));
if (attrs) { delete[] attrs; } if (attrs) { free(attrs); }
return; return;
} }
break; // found, exit loop 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; attrs_len = actual_attr_len;
} else { } else {
attrs_len = 2; attrs_len = attr_item_len;
attrs = new uint8_t[attrs_len]; attrs = (uint8_t*) calloc(attrs_len, 1);
attrs[0] = val & 0xFF; // little endian attrs[0 + attr_item_offset] = val & 0xFF; // little endian
attrs[1] = val >> 8; attrs[1 + attr_item_offset] = val >> 8;
} }
if (attrs_len > 0) { 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(); ResponseCmndDone();
} else { } else {
ResponseCmndChar_P(PSTR("Missing parameters")); 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_write = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_WRITE));
const JsonVariant &val_publish = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_REPORT)); const JsonVariant &val_publish = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_REPORT));
const JsonVariant &val_response = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_RESPONSE)); 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) { 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; return;
} }
// from here we have one and only one command // from here we have one and only one command
@ -608,7 +710,7 @@ void CmndZbSend(void) {
} else if (nullptr != &val_read) { } else if (nullptr != &val_read) {
// "Read":{...attributes...}, "Read":attribute or "Read":[...attributes...] // "Read":{...attributes...}, "Read":attribute or "Read":[...attributes...]
// we accept eitehr a number, a string, an array of numbers/strings, or a JSON object // 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) { } else if (nullptr != &val_write) {
// only KSON object // only KSON object
if (!val_write.is<JsonObject>()) { if (!val_write.is<JsonObject>()) {
@ -618,7 +720,7 @@ void CmndZbSend(void) {
// "Write":{...attributes...} // "Write":{...attributes...}
ZbSendReportWrite(val_write, device, groupaddr, cluster, endpoint, manuf, ZCL_WRITE_ATTRIBUTES); ZbSendReportWrite(val_write, device, groupaddr, cluster, endpoint, manuf, ZCL_WRITE_ATTRIBUTES);
} else if (nullptr != &val_publish) { } else if (nullptr != &val_publish) {
// "Report":{...attributes...} // "Publish":{...attributes...}
// only KSON object // only KSON object
if (!val_publish.is<JsonObject>()) { if (!val_publish.is<JsonObject>()) {
ResponseCmndChar_P(PSTR("Missing parameters")); ResponseCmndChar_P(PSTR("Missing parameters"));
@ -633,6 +735,18 @@ void CmndZbSend(void) {
return; return;
} }
ZbSendReportWrite(val_response, device, groupaddr, cluster, endpoint, manuf, ZCL_READ_ATTRIBUTES_RESPONSE); 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 { } else {
Response_P(PSTR("Missing zigbee 'Send', 'Write', 'Report' or 'Response'")); Response_P(PSTR("Missing zigbee 'Send', 'Write', 'Report' or 'Response'"));
return; return;
@ -675,18 +789,36 @@ void ZbBindUnbind(bool unbind) { // false = bind, true = unbind
// look for source endpoint // look for source endpoint
const JsonVariant &val_endpoint = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_ENDPOINT)); const JsonVariant &val_endpoint = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_ENDPOINT));
if (nullptr != &val_endpoint) { endpoint = strToUInt(val_endpoint); } if (nullptr != &val_endpoint) { endpoint = strToUInt(val_endpoint); }
else { endpoint = zigbee_devices.findFirstEndpoint(srcDevice); }
// look for source cluster // look for source cluster
const JsonVariant &val_cluster = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_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 // Either Device address
// In this case the following parameters are mandatory // In this case the following parameters are mandatory
// - "ToDevice" and the device must have a known IEEE address // - "ToDevice" and the device must have a known IEEE address
// - "ToEndpoint" // - "ToEndpoint"
const JsonVariant &dst_device = GetCaseInsensitive(json, PSTR("ToDevice")); const JsonVariant &dst_device = GetCaseInsensitive(json, PSTR("ToDevice"));
if (nullptr != &dst_device) {
dstDevice = zigbee_devices.parseDeviceParam(dst_device.as<char*>()); // If no target is specified, we default to coordinator 0x0000
if (BAD_SHORTADDR == dstDevice) { ResponseCmndChar_P(PSTR("Invalid parameter")); return; } 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) { if (0x0000 == dstDevice) {
dstLongAddr = localIEEEAddr; dstLongAddr = localIEEEAddr;
} else { } else {
@ -695,13 +827,10 @@ void ZbBindUnbind(bool unbind) { // false = bind, true = unbind
if (0 == dstLongAddr) { ResponseCmndChar_P(PSTR("Unknown dest IEEE address")); return; } if (0 == dstLongAddr) { ResponseCmndChar_P(PSTR("Unknown dest IEEE address")); return; }
const JsonVariant &val_toendpoint = GetCaseInsensitive(json, PSTR("ToEndpoint")); 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 // 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("Cannot have both \"ToDevice\" and \"ToGroup\"")); return; }
if (!&to_group && !dstLongAddr) { ResponseCmndChar_P(PSTR("Missing \"ToDevice\" or \"ToGroup\"")); return; } if (!&to_group && !dstLongAddr) { ResponseCmndChar_P(PSTR("Missing \"ToDevice\" or \"ToGroup\"")); return; }