Add Zigbee additional commands and sending messages to control devices (#6095)

This commit is contained in:
Stephan Hadinger 2019-10-13 12:56:52 +02:00
parent 36371fc3a7
commit 0092bf766c
9 changed files with 340 additions and 82 deletions

View File

@ -1,6 +1,7 @@
/*********************************************************************************************\
* 6.6.0.18 20191010
* Add command DimmerRange in Light module to support 2 byte dimming ranges from Tuya
* Add Zigbee additional commands and sending messages to control devices (#6095)
*
* 6.6.0.17 20191009
* Add command SetOption34 0..255 to set backlog delay. Default value is 200 (mSeconds) (#6562)

View File

@ -460,13 +460,17 @@
// Commands xdrv_23_zigbee.ino
#define D_CMND_ZIGBEE_PERMITJOIN "ZigbeePermitJoin"
#define D_CMND_ZIGBEE_STATUS "ZigbeeStatus"
#define D_CMND_ZIGBEE_RESET "ZigbeeReset"
#define D_JSON_ZIGBEE_CC2530 "CC2530"
#define D_CMND_ZIGBEEZNPSEND "ZigbeeZNPSend"
#define D_JSON_ZIGBEE_STATUS "ZigbeeStatus"
#define D_JSON_ZIGBEEZNPRECEIVED "ZigbeeZNPReceived"
#define D_JSON_ZIGBEEZNPSENT "ZigbeeZNPSent"
#define D_JSON_ZIGBEEZCL_RECEIVED "ZigbeeZCLReceived"
#define D_JSON_ZIGBEEZCL_RAW_RECEIVED "ZigbeeZCLRawReceived"
#define D_JSON_ZIGBEEZCLSENT "ZigbeeZCLSent"
#define D_CMND_ZIGBEE_ZCL_SEND "ZigbeeZCLSend"
#define D_JSON_ZIGBEE_ZCL_SENT "ZigbeeZCLSent"
#define D_CMND_ZIGBEE_PROBE "ZigbeeProbe"
// Commands xdrv_25_A4988_Stepper.ino
#ifdef USE_A4988_Stepper

View File

@ -98,6 +98,15 @@ public:
return _buf->len;
}
size_t addBuffer(const uint8_t *buf2, size_t len2) {
if (len() + len2 <= size()) {
for (uint32_t i = 0; i < len2; i++) {
_buf->buf[_buf->len++] = pgm_read_byte(&buf2[i]);
}
}
return _buf->len;
}
size_t addBuffer(const char *buf2, size_t len2) {
if (len() + len2 <= size()) {
for (uint32_t i = 0; i < len2; i++) {

View File

@ -19,6 +19,8 @@
#ifdef USE_ZIGBEE
#define ZIGBEE_VERBOSE // output versbose MQTT Zigbee logs. Will remain active for now
typedef uint64_t Z_IEEEAddress;
typedef uint16_t Z_ShortAddress;

View File

@ -25,6 +25,8 @@
typedef struct Z_Device {
uint16_t shortaddr; // unique key if not null, or unspecified if null
uint64_t longaddr; // 0x00 means unspecified
uint32_t firstSeen; // date when the device was first seen
uint32_t lastSeen; // date when the device was last seen
String manufacturerId;
String modelId;
String friendlyName;
@ -62,6 +64,9 @@ public:
void setModelId(uint16_t shortaddr, const char * str);
void setFriendlyNameId(uint16_t shortaddr, const char * str);
// device just seen on the network, update the lastSeen field
void updateLastSeen(uint16_t shortaddr);
// Dump json
String dump(uint8_t dump_mode) const;
@ -83,6 +88,12 @@ private:
int32_t findShortAddr(uint16_t shortaddr);
int32_t findLongAddr(uint64_t longaddr);
void _updateLastSeen(Z_Device &device) {
if (&device != nullptr) {
device.lastSeen = Rtc.utc_time;
}
};
// Create a new entry in the devices list - must be called if it is sure it does not already exist
Z_Device & createDeviceEntry(uint16_t shortaddr, uint64_t longaddr = 0);
};
@ -140,6 +151,7 @@ int32_t Z_Devices::findClusterEndpoint(const std::vector<uint32_t> & vecOfEleme
Z_Device & Z_Devices::createDeviceEntry(uint16_t shortaddr, uint64_t longaddr) {
if (!shortaddr && !longaddr) { return *(Z_Device*) nullptr; } // it is not legal to create an enrty with both short/long addr null
Z_Device device = { shortaddr, longaddr,
Rtc.utc_time, Rtc.utc_time,
String(), // ManufId
String(), // DeviceId
String(), // FriendlyName
@ -223,17 +235,19 @@ void Z_Devices::updateDevice(uint16_t shortaddr, uint64_t longaddr) {
if ((s_found >= 0) && (l_found >= 0)) { // both shortaddr and longaddr are already registered
if (s_found == l_found) {
; // short/long addr match, all good
updateLastSeen(shortaddr); // short/long addr match, all good
} else { // they don't match
// the device with longaddr got a new shortaddr
_devices[l_found].shortaddr = shortaddr; // update the shortaddr corresponding to the longaddr
// erase the previous shortaddr
_devices.erase(_devices.begin() + s_found);
updateLastSeen(shortaddr);
}
} else if (s_found >= 0) {
// shortaddr already exists but longaddr not
// add the longaddr to the entry
_devices[s_found].longaddr = longaddr;
updateLastSeen(shortaddr);
} else if (l_found >= 0) {
// longaddr entry exists, update shortaddr
_devices[l_found].shortaddr = shortaddr;
@ -253,7 +267,8 @@ void Z_Devices::addEndoint(uint16_t shortaddr, uint8_t endpoint) {
uint32_t ep_profile = (endpoint << 16);
Z_Device &device = getShortAddr(shortaddr);
if (&device == nullptr) { return; } // don't crash if not found
if (findEndpointInVector(device.endpoints, ep_profile) < 0) { // TODO search only on enpoint
_updateLastSeen(device);
if (findEndpointInVector(device.endpoints, ep_profile) < 0) {
device.endpoints.push_back(ep_profile);
}
}
@ -263,8 +278,9 @@ void Z_Devices::addEndointProfile(uint16_t shortaddr, uint8_t endpoint, uint16_t
uint32_t ep_profile = (endpoint << 16) | profileId;
Z_Device &device = getShortAddr(shortaddr);
if (&device == nullptr) { return; } // don't crash if not found
_updateLastSeen(device);
int32_t found = findEndpointInVector(device.endpoints, ep_profile);
if (found < 0) { // TODO search only on enpoint
if (found < 0) {
device.endpoints.push_back(ep_profile);
} else {
device.endpoints[found] = ep_profile;
@ -275,6 +291,7 @@ void Z_Devices::addCluster(uint16_t shortaddr, uint8_t endpoint, uint16_t cluste
if (!shortaddr) { return; }
Z_Device & device = getShortAddr(shortaddr);
if (&device == nullptr) { return; } // don't crash if not found
_updateLastSeen(device);
uint32_t ep_cluster = (endpoint << 16) | cluster;
if (!out) {
if (!findInVector(device.clusters_in, ep_cluster)) {
@ -290,6 +307,8 @@ void Z_Devices::addCluster(uint16_t shortaddr, uint8_t endpoint, uint16_t cluste
// Look for the best endpoint match to send a command for a specific Cluster ID
// return 0x00 if none found
uint8_t Z_Devices::findClusterEndpointIn(uint16_t shortaddr, uint16_t cluster){
int32_t short_found = findShortAddr(shortaddr);
if (short_found < 0) return 0; // avoid creating an entry if the device was never seen
Z_Device &device = getShortAddr(shortaddr);
if (&device == nullptr) { return 0; } // don't crash if not found
int32_t found = findClusterEndpoint(device.clusters_in, cluster);
@ -304,19 +323,29 @@ uint8_t Z_Devices::findClusterEndpointIn(uint16_t shortaddr, uint16_t cluster){
void Z_Devices::setManufId(uint16_t shortaddr, const char * str) {
Z_Device & device = getShortAddr(shortaddr);
if (&device == nullptr) { return; } // don't crash if not found
_updateLastSeen(device);
device.manufacturerId = str;
}
void Z_Devices::setModelId(uint16_t shortaddr, const char * str) {
Z_Device & device = getShortAddr(shortaddr);
if (&device == nullptr) { return; } // don't crash if not found
_updateLastSeen(device);
device.modelId = str;
}
void Z_Devices::setFriendlyNameId(uint16_t shortaddr, const char * str) {
Z_Device & device = getShortAddr(shortaddr);
if (&device == nullptr) { return; } // don't crash if not found
_updateLastSeen(device);
device.friendlyName = str;
}
// device just seen on the network, update the lastSeen field
void Z_Devices::updateLastSeen(uint16_t shortaddr) {
Z_Device & device = getShortAddr(shortaddr);
if (&device == nullptr) { return; } // don't crash if not found
_updateLastSeen(device);
}
// Dump the internal memory of Zigbee devices
// Mode = 1: simple dump of devices addresses and names
// Mode = 2: Mode 1 + also dump the endpoints, profiles and clusters

View File

@ -53,6 +53,7 @@ public:
uint8_t srcendpoint, uint8_t dstendpoint, uint8_t wasbroadcast,
uint8_t linkquality, uint8_t securityuse, uint8_t seqnumber,
uint32_t timestamp) {
#ifdef ZIGBEE_VERBOSE
char hex_char[_payload.len()*2+2];
ToHex_P((unsigned char*)_payload.getBuffer(), _payload.len(), hex_char, sizeof(hex_char));
Response_P(PSTR("{\"" D_JSON_ZIGBEEZCL_RECEIVED "\":{"
@ -73,6 +74,7 @@ public:
ResponseJsonEnd(); // append '}'
MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_ZIGBEEZCL_RECEIVED));
XdrvRulesProcess();
#endif
}
static ZCLFrame parseRawFrame(const SBuffer &buf, uint8_t offset, uint8_t len, uint16_t clusterid, uint16_t groupid) { // parse a raw frame and build the ZCL frame object
@ -134,17 +136,6 @@ private:
SBuffer _payload;
};
char Hex36Char(uint8_t value) {
// convert an integer from 0 to 46, to a single digit 0-9A-Z
if (value < 10) {
return '0' + value;
} else if (value < 46) {
return 'A' + value - 10;
} else {
return '?'; // out of range
}
}
// Zigbee ZCL converters
// from https://github.com/Koenkk/zigbee-shepherd-converters/blob/638d29f0cace6343052b9a4e7fd60980fa785479/converters/fromZigbee.js#L55
@ -299,19 +290,29 @@ uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer
i += buf.get8(i) + 1;
break;
// TODO
case 0x08: // data8
i++;
break;
case 0x18: // map8
i++;
{
uint8_t uint8_val = buf.get8(i);
i += 1;
json[attrid_str] = uint8_val;
}
break;
case 0x09: // data16
case 0x19: // map16
i += 2;
{
uint16_t uint16_val = buf.get16(i);
i += 2;
json[attrid_str] = uint16_val;
}
break;
case 0x0B: // data32
case 0x1B: // map32
i += 4;
{
uint32_t uint32_val = buf.get32(i);
i += 4;
json[attrid_str] = uint32_val;
}
break;
// enum
case 0x30: // enum8
@ -319,6 +320,7 @@ uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer
i += attrtype - 0x2F;
break;
// TODO
case 0x39: // float
i += 4;
break;
@ -345,9 +347,7 @@ uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer
break;
// Other un-implemented data types
case 0x09: // data16
case 0x0A: // data24
case 0x0B: // data32
case 0x0C: // data40
case 0x0D: // data48
case 0x0E: // data56
@ -381,7 +381,6 @@ uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer
// First pass, parse all attributes in their native format
// The key is 32 bits, high 16 bits is cluserid, low 16 bits is attribute id
void ZCLFrame::parseRawAttributes(JsonObject& json, uint8_t offset) {
uint32_t i = offset;
uint32_t len = _payload.len();
@ -390,9 +389,9 @@ void ZCLFrame::parseRawAttributes(JsonObject& json, uint8_t offset) {
uint16_t attrid = _payload.get16(i);
i += 2;
char shortaddr[16];
snprintf_P(shortaddr, sizeof(shortaddr), PSTR("%c_%04X_%04X"),
Hex36Char(_cmd_id), _cluster_id, attrid);
char key[16];
snprintf_P(key, sizeof(key), PSTR("%04X_%02X_%04X"),
_cluster_id, _cmd_id, attrid);
// exception for Xiaomi lumi.weather - specific field to be treated as octet and not char
if ((0x0000 == _cluster_id) && (0xFF01 == attrid)) {
@ -400,7 +399,7 @@ void ZCLFrame::parseRawAttributes(JsonObject& json, uint8_t offset) {
_payload.set8(i, 0x41); // change type from 0x42 to 0x41
}
}
i += parseSingleAttribute(json, shortaddr, _payload, i, len);
i += parseSingleAttribute(json, key, _payload, i, len);
}
}
@ -415,11 +414,11 @@ void ZCLFrame::parseReadAttributes(JsonObject& json, uint8_t offset) {
uint8_t status = _payload.get8(i++);
if (0 == status) {
char shortaddr[16];
snprintf_P(shortaddr, sizeof(shortaddr), PSTR("%c_%04X_%04X"),
Hex36Char(_cmd_id), _cluster_id, attrid);
char key[16];
snprintf_P(key, sizeof(key), PSTR("%04X_%02X_%04X"),
_cluster_id, _cmd_id, attrid);
i += parseSingleAttribute(json, shortaddr, _payload, i, len);
i += parseSingleAttribute(json, key, _payload, i, len);
}
}
}
@ -432,7 +431,7 @@ void ZCLFrame::parseClusterSpecificCommand(JsonObject& json, uint8_t offset) {
uint32_t len = _payload.len();
char attrid_str[12];
snprintf_P(attrid_str, sizeof(attrid_str), PSTR("s_%04X_%02X"), _cluster_id, _cmd_id);
snprintf_P(attrid_str, sizeof(attrid_str), PSTR("%04X!%02X"), _cmd_id, _cluster_id);
char hex_char[_payload.len()*2+2];
ToHex_P((unsigned char*)_payload.getBuffer(), _payload.len(), hex_char, sizeof(hex_char));
@ -456,51 +455,51 @@ const float Z_10 PROGMEM = 10.0f;
// list of post-processing directives
const Z_AttributeConverter Z_PostProcess[] = {
{ "?_0000_0004", nullptr, &Z_ManufKeep, nullptr }, // record Manufacturer
{ "?_0000_0005", nullptr, &Z_ModelKeep, nullptr }, // record Model
{ "0000_0?_0004", nullptr, &Z_ManufKeep, nullptr }, // record Manufacturer
{ "0000_0?_0005", nullptr, &Z_ModelKeep, nullptr }, // record Model
{ "?_0000_0000", "ZCLVersion", &Z_Copy, nullptr },
{ "?_0000_0001", "AppVersion", &Z_Copy, nullptr },
{ "?_0000_0002", "StackVersion", &Z_Copy, nullptr },
{ "?_0000_0003", "HWVersion", &Z_Copy, nullptr },
{ "?_0000_0004", "Manufacturer", &Z_Copy, nullptr },
{ "?_0000_0005", D_JSON_MODEL D_JSON_ID, &Z_Copy, nullptr },
{ "?_0000_0006", "DateCode", &Z_Copy, nullptr },
{ "?_0000_0007", "PowerSource", &Z_Copy, nullptr },
{ "?_0000_4000", "SWBuildID", &Z_Copy, nullptr },
{ "A_0000_????", nullptr, &Z_Remove, nullptr }, // Remove all other values
{ "0000_0?_0000", "ZCLVersion", &Z_Copy, nullptr },
{ "0000_0?_0001", "AppVersion", &Z_Copy, nullptr },
{ "0000_0?_0002", "StackVersion", &Z_Copy, nullptr },
{ "0000_0?_0003", "HWVersion", &Z_Copy, nullptr },
{ "0000_0?_0004", "Manufacturer", &Z_Copy, nullptr },
{ "0000_0?_0005", D_JSON_MODEL D_JSON_ID, &Z_Copy, nullptr },
{ "0000_0?_0006", "DateCode", &Z_Copy, nullptr },
{ "0000_0?_0007", "PowerSource", &Z_Copy, nullptr },
{ "0000_0?_4000", "SWBuildID", &Z_Copy, nullptr },
{ "0000_0?_????", nullptr, &Z_Remove, nullptr }, // Remove all other values
{ "A_0400_0000", D_JSON_ILLUMINANCE, &Z_Copy, nullptr }, // Illuminance (in Lux)
{ "A_0400_0004", "LightSensorType", &Z_Copy, nullptr }, // LightSensorType
{ "A_0400_????", nullptr, &Z_Remove, nullptr }, // Remove all other values
{ "0400_0A_0000", D_JSON_ILLUMINANCE, &Z_Copy, nullptr }, // Illuminance (in Lux)
{ "0400_0A_0004", "LightSensorType", &Z_Copy, nullptr }, // LightSensorType
{ "0400_0A_????", nullptr, &Z_Remove, nullptr }, // Remove all other values
{ "A_0401_0000", "LevelStatus", &Z_Copy, nullptr }, // Illuminance (in Lux)
{ "A_0401_0001", "LightSensorType", &Z_Copy, nullptr }, // LightSensorType
{ "A_0401_????", nullptr, &Z_Remove, nullptr }, // Remove all other values
{ "0401_0A_0000", "LevelStatus", &Z_Copy, nullptr }, // Illuminance (in Lux)
{ "0401_0A_0001", "LightSensorType", &Z_Copy, nullptr }, // LightSensorType
{ "0401_0A_????", nullptr, &Z_Remove, nullptr }, // Remove all other values
{ "A_0402_0000", D_JSON_TEMPERATURE, &Z_ConvFloatDivider, (void*) &Z_100 }, // Temperature
{ "A_0402_????", nullptr, &Z_Remove, nullptr }, // Remove all other values
{ "0402_0A_0000", D_JSON_TEMPERATURE, &Z_ConvFloatDivider, (void*) &Z_100 }, // Temperature
{ "0402_0A_????", nullptr, &Z_Remove, nullptr }, // Remove all other values
{ "A_0403_0000", D_JSON_PRESSURE_UNIT, &Z_Const_Keep, (void*) D_UNIT_PRESSURE}, // Pressure Unit
{ "A_0403_0000", D_JSON_PRESSURE, &Z_Copy, nullptr }, // Pressure
{ "A_0403_????", nullptr, &Z_Remove, nullptr }, // Remove all other Pressure values
{ "0403_0A_0000", D_JSON_PRESSURE_UNIT, &Z_Const_Keep, (void*) D_UNIT_PRESSURE}, // Pressure Unit
{ "0403_0A_0000", D_JSON_PRESSURE, &Z_Copy, nullptr }, // Pressure
{ "0403_0A_????", nullptr, &Z_Remove, nullptr }, // Remove all other Pressure values
{ "A_0404_0000", D_JSON_FLOWRATE, &Z_ConvFloatDivider, (void*) &Z_10 }, // Flow (in m3/h)
{ "A_0404_????", nullptr, &Z_Remove, nullptr }, // Remove all other values
{ "0404_0A_0000", D_JSON_FLOWRATE, &Z_ConvFloatDivider, (void*) &Z_10 }, // Flow (in m3/h)
{ "0404_0A_????", nullptr, &Z_Remove, nullptr }, // Remove all other values
{ "A_0405_0000", D_JSON_HUMIDITY, &Z_ConvFloatDivider, (void*) &Z_100 }, // Humidity
{ "A_0405_????", nullptr, &Z_Remove, nullptr }, // Remove all other values
{ "0405_0A_0000", D_JSON_HUMIDITY, &Z_ConvFloatDivider, (void*) &Z_100 }, // Humidity
{ "0405_0A_????", nullptr, &Z_Remove, nullptr }, // Remove all other values
{ "A_0406_0000", "Occupancy", &Z_Copy, nullptr }, // Occupancy (map8)
{ "A_0406_0001", "OccupancySensorType", &Z_Copy, nullptr }, // OccupancySensorType
{ "A_0406_????", nullptr, &Z_Remove, nullptr }, // Remove all other values
{ "0406_0A_0000", "Occupancy", &Z_Copy, nullptr }, // Occupancy (map8)
{ "0406_0A_0001", "OccupancySensorType", &Z_Copy, nullptr }, // OccupancySensorType
{ "0406_0A_????", nullptr, &Z_Remove, nullptr }, // Remove all other values
// Cmd 0x0A - Cluster 0x0000, attribute 0xFF01 - proprietary
{ "A_0000_FF01", nullptr, &Z_AqaraSensor, nullptr }, // Occupancy (map8)
{ "0000_0A_FF01", nullptr, &Z_AqaraSensor, nullptr }, // Occupancy (map8)
// // 0x0b04 Electrical Measurement
// { "A_0B04_0100", "DCVoltage", &Z_Copy, nullptr }, // Occupancy (map8)
// { "A_0B04_0001", "OccupancySensorType", &Z_Copy, nullptr }, // OccupancySensorType
// { "A_0B04_????", "", &Z_Remove, nullptr }, // Remove all other values
// { "0B04_0100", "DCVoltage", &Z_Copy, nullptr }, //
// { "0B04_0001", "OccupancySensorType", &Z_Copy, nullptr }, //
// { "0B04_????", "", &Z_Remove, nullptr }, //
};
@ -573,13 +572,13 @@ int32_t Z_AqaraSensor(uint16_t shortaddr, JsonObject& json, const char *name, Js
}
// ======================================================================
#define ZCL_MODELID "A_0000_0005" // Cmd 0x0A - Cluster 0x0000, attribute 0x05
#define ZCL_TEMPERATURE "A_0402_0000" // Cmd 0x0A - Cluster 0x0402, attribute 0x00
#define ZCL_PRESSURE "A_0403_0000" // Cmd 0x0A - Cluster 0x0403, attribute 0x00
#define ZCL_PRESSURE_SCALED "A_0403_0010" // Cmd 0x0A - Cluster 0x0403, attribute 0x10
#define ZCL_PRESSURE_SCALE "A_0403_0014" // Cmd 0x0A - Cluster 0x0403, attribute 0x14
#define ZCL_HUMIDITY "A_0405_0000" // Cmd 0x0A - Cluster 0x0403, attribute 0x00
#define ZCL_LUMI_WEATHER "A_0000_FF01" // Cmd 0x0A - Cluster 0x0000, attribute 0xFF01 - proprietary
// #define ZCL_MODELID "A_0000_0005" // Cmd 0x0A - Cluster 0x0000, attribute 0x05
// #define ZCL_TEMPERATURE "A_0402_0000" // Cmd 0x0A - Cluster 0x0402, attribute 0x00
// #define ZCL_PRESSURE "A_0403_0000" // Cmd 0x0A - Cluster 0x0403, attribute 0x00
// #define ZCL_PRESSURE_SCALED "A_0403_0010" // Cmd 0x0A - Cluster 0x0403, attribute 0x10
// #define ZCL_PRESSURE_SCALE "A_0403_0014" // Cmd 0x0A - Cluster 0x0403, attribute 0x14
// #define ZCL_HUMIDITY "A_0405_0000" // Cmd 0x0A - Cluster 0x0403, attribute 0x00
// #define ZCL_LUMI_WEATHER "A_0000_FF01" // Cmd 0x0A - Cluster 0x0000, attribute 0xFF01 - proprietary
// Cluster Specific commands
#define ZCL_OO_OFF "s_0006_00" // Cluster 0x0006, cmd 0x00 - On/Off - Off

View File

@ -322,10 +322,10 @@ static const Zigbee_Instruction zb_prog[] PROGMEM = {
ZI_WAIT(10000) // wait for 10 seconds for Tasmota to stabilize
ZI_ON_ERROR_GOTO(50)
ZI_MQTT_STATUS(ZIGBEE_STATUS_BOOT, "Booting")
//ZI_MQTT_STATUS(ZIGBEE_STATUS_BOOT, "Booting")
//ZI_LOG(LOG_LEVEL_INFO, "ZIG: rebooting device")
ZI_SEND(ZBS_RESET) // reboot cc2530 just in case we rebooted ESP8266 but not cc2530
ZI_WAIT_RECV(5000, ZBR_RESET) // timeout 5s
ZI_WAIT_RECV_FUNC(5000, ZBR_RESET, &Z_Reboot) // timeout 5s
ZI_WAIT(100)
ZI_LOG(LOG_LEVEL_INFO, "ZIG: checking device configuration")
ZI_SEND(ZBS_ZNPHC) // check value of ZNP Has Configured
@ -356,7 +356,7 @@ ZI_SEND(ZBS_STARTUPFROMAPP) // start coordinator
ZI_WAIT_UNTIL(5000, AREQ_STARTUPFROMAPP) // wait for async message that coordinator started
ZI_SEND(ZBS_GETDEVICEINFO) // GetDeviceInfo
ZI_WAIT_RECV_FUNC(2000, ZBR_GETDEVICEINFO, &Z_ReceiveDeviceInfo)
//ZI_WAIT_RECV(2000, ZBR_GETDEVICEINFO) // TODO memorize info
//ZI_WAIT_RECV(2000, ZBR_GETDEVICEINFO) // memorize info
ZI_SEND(ZBS_ZDO_NODEDESCREQ) // Z_ZDO:nodeDescReq
ZI_WAIT_RECV(1000, ZBR_ZDO_NODEDESCREQ)
ZI_WAIT_UNTIL(5000, AREQ_ZDO_NODEDESCRSP)

View File

@ -73,6 +73,39 @@ int32_t Z_CheckNVWrite(int32_t res, class SBuffer &buf) {
}
}
const char Z_RebootReason[] PROGMEM = "Power-up|External|Watchdog";
int32_t Z_Reboot(int32_t res, class SBuffer &buf) {
// print information about the reboot of device
// 4180.02.02.00.02.06.03
//
uint8_t reason = buf.get8(2);
uint8_t transport_rev = buf.get8(3);
uint8_t product_id = buf.get8(4);
uint8_t major_rel = buf.get8(5);
uint8_t minor_rel = buf.get8(6);
uint8_t hw_rev = buf.get8(7);
char reason_str[12];
if (reason > 3) { reason = 3; }
GetTextIndexed(reason_str, sizeof(reason_str), reason, Z_RebootReason);
Response_P(PSTR("{\"" D_JSON_ZIGBEE_STATUS "\":{"
"\"Status\":%d,\"Message\":\"%s\",\"RestartReason\":\"%s\""
",\"MajorRel\":%d,\"MinorRel\":%d}}"),
ZIGBEE_STATUS_BOOT, "CC2530 booted", reason_str,
major_rel, minor_rel);
MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_ZIGBEE_STATUS));
XdrvRulesProcess();
if ((0x02 == major_rel) && (0x06 == minor_rel)) {
return 0; // version 2.6.x is ok
} else {
return ZIGBEE_LABEL_UNSUPPORTED_VERSION; // abort
}
}
int32_t Z_ReceiveCheckVersion(int32_t res, class SBuffer &buf) {
// check that the version is supported
// typical version for ZNP 1.2
@ -178,6 +211,8 @@ int32_t Z_ReceiveNodeDesc(int32_t res, const class SBuffer &buf) {
uint8_t descriptorCapabilities = buf.get8(19);
if (0 == status) {
zigbee_devices.updateLastSeen(nwkAddr);
uint8_t deviceType = logicalType & 0x7; // 0=coordinator, 1=router, 2=end device
if (deviceType > 3) { deviceType = 3; }
bool complexDescriptorAvailable = (logicalType & 0x08) ? 1 : 0;
@ -290,9 +325,9 @@ int32_t Z_ReceiveSimpleDesc(int32_t res, const class SBuffer &buf) {
XdrvRulesProcess();
uint8_t cluster = zigbee_devices.findClusterEndpointIn(nwkAddr, 0x0000);
Serial.printf(">>> Endpoint is 0x%02X for cluster 0x%04X\n", cluster, 0x0000);
//Serial.printf(">>> Endpoint is 0x%02X for cluster 0x%04X\n", cluster, 0x0000);
if (cluster) {
Z_SendAFInfoRequest(nwkAddr, cluster, 0x0000, 0x01); // TODO
Z_SendAFInfoRequest(nwkAddr, cluster, 0x0000, 0x01); // TODO, do we need tarnsacId counter?
}
}
return -1;
@ -335,12 +370,15 @@ int32_t Z_ReceiveAfIncomingMessage(int32_t res, const class SBuffer &buf) {
uint32_t timestamp = buf.get32(13);
uint8_t seqnumber = buf.get8(17);
zigbee_devices.updateLastSeen(srcaddr);
ZCLFrame zcl_received = ZCLFrame::parseRawFrame(buf, 19, buf.get8(18), clusterid, groupid);
#ifdef ZIGBEE_VERBOSE
zcl_received.publishMQTTReceived(groupid, clusterid, srcaddr,
srcendpoint, dstendpoint, wasbroadcast,
linkquality, securityuse, seqnumber,
timestamp);
#endif
char shortaddr[8];
snprintf_P(shortaddr, sizeof(shortaddr), PSTR("0x%04X"), srcaddr);

View File

@ -23,6 +23,7 @@
const uint32_t ZIGBEE_BUFFER_SIZE = 256; // Max ZNP frame is SOF+LEN+CMD1+CMD2+250+FCS = 255
const uint8_t ZIGBEE_SOF = 0xFE;
const uint8_t ZIGBEE_SOF_ALT = 0xFF;
//#define Z_USE_SOFTWARE_SERIAL
@ -36,10 +37,12 @@ TasmotaSerial *ZigbeeSerial = nullptr;
const char kZigbeeCommands[] PROGMEM = "|" D_CMND_ZIGBEEZNPSEND "|" D_CMND_ZIGBEE_PERMITJOIN
"|" D_CMND_ZIGBEE_STATUS;
"|" D_CMND_ZIGBEE_STATUS "|" D_CMND_ZIGBEE_RESET "|" D_CMND_ZIGBEE_ZCL_SEND
"|" D_CMND_ZIGBEE_PROBE;
void (* const ZigbeeCommand[])(void) PROGMEM = { &CmndZigbeeZNPSend, &CmndZigbeePermitJoin,
&CmndZigbeeStatus };
&CmndZigbeeStatus, &CmndZigbeeReset, &CmndZigbeeZCLSend,
&CmndZigbeeProbe };
int32_t ZigbeeProcessInput(class SBuffer &buf) {
if (!zigbee.state_machine) { return -1; } // if state machine is stopped, send 'ignore' message
@ -140,6 +143,13 @@ void ZigbeeInput(void)
if (0 == zigbee_buffer->len()) { // make sure all variables are correctly initialized
zigbee_frame_len = 5;
fcs = ZIGBEE_SOF;
// there is a rare race condition when an interrupt occurs when receiving the first byte
// in this case the first bit (lsb) is missed and Tasmota receives 0xFF instead of 0xFE
// We forgive this mistake, and next bytes are automatically resynchronized
if (ZIGBEE_SOF_ALT == zigbee_in_byte) {
AddLog_P2(LOG_LEVEL_INFO, PSTR("ZigbeeInput forgiven first byte %02X (only for statistics)"), zigbee_in_byte);
zigbee_in_byte = ZIGBEE_SOF;
}
}
if ((0 == zigbee_buffer->len()) && (ZIGBEE_SOF != zigbee_in_byte)) {
@ -189,10 +199,12 @@ void ZigbeeInput(void)
SBuffer znp_buffer = zigbee_buffer->subBuffer(2, zigbee_frame_len - 3); // remove SOF, LEN and FCS
#ifdef ZIGBEE_VERBOSE
ToHex_P((unsigned char*)znp_buffer.getBuffer(), znp_buffer.len(), hex_char, sizeof(hex_char));
Response_P(PSTR("{\"" D_JSON_ZIGBEEZNPRECEIVED "\":\"%s\"}"), hex_char);
MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_ZIGBEEZNPRECEIVED));
XdrvRulesProcess();
#endif
// now process the message
ZigbeeProcessInput(znp_buffer);
@ -234,6 +246,34 @@ void ZigbeeInit(void)
* Commands
\*********************************************************************************************/
uint32_t strToUInt(const JsonVariant &val) {
// if the string starts with 0x, it is considered Hex, otherwise it is an int
if (val.is<unsigned int>()) {
return val.as<unsigned int>();
} else {
if (val.is<char*>()) {
return strtoull(val.as<char*>(), nullptr, 0);
}
}
return 0; // couldn't parse anything
}
const unsigned char ZIGBEE_FACTORY_RESET[] PROGMEM = "2112000F0100"; // Z_SREQ | Z_SYS, SYS_OSAL_NV_DELETE, 0x0F00 id, 0x0001 len
// Do a factory reset of the CC2530
void CmndZigbeeReset(void) {
if (ZigbeeSerial) {
switch (XdrvMailbox.payload) {
case 1:
ZigbeeZNPSend(ZIGBEE_FACTORY_RESET, sizeof(ZIGBEE_FACTORY_RESET));
restart_flag = 2;
ResponseCmndChar(D_JSON_ZIGBEE_CC2530 " " D_JSON_RESET_AND_RESTARTING);
break;
default:
ResponseCmndChar(D_JSON_ONE_TO_RESET);
}
}
}
void CmndZigbeeStatus(void) {
if (ZigbeeSerial) {
String dump = zigbee_devices.dump(XdrvMailbox.payload);
@ -288,12 +328,148 @@ void ZigbeeZNPSend(const uint8_t *msg, size_t len) {
ZigbeeSerial->write(fcs); // finally send fcs checksum byte
AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("ZNPSend FCS %02X"), fcs);
}
#ifdef ZIGBEE_VERBOSE
// Now send a MQTT message to report the sent message
char hex_char[(len * 2) + 2];
Response_P(PSTR("{\"" D_JSON_ZIGBEEZNPSENT "\":\"%s\"}"),
ToHex_P(msg, len, hex_char, sizeof(hex_char)));
MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_ZIGBEEZNPSENT));
XdrvRulesProcess();
#endif
}
void ZigbeeZCLSend(uint16_t dtsAddr, uint16_t clusterId, uint8_t endpoint, uint8_t cmdId, bool clusterSpecific, const uint8_t *msg, size_t len, uint8_t transacId = 1) {
SBuffer buf(25+len);
buf.add8(Z_SREQ | Z_AF); // 24
buf.add8(AF_DATA_REQUEST); // 01
buf.add16(dtsAddr);
buf.add8(endpoint); // dest endpoint
buf.add8(0x01); // source endpoint
buf.add16(clusterId);
buf.add8(transacId); // transacId
buf.add8(0x30); // 30 options
buf.add8(0x1E); // 1E radius
buf.add8(3 + len);
buf.add8(0x10 | (clusterSpecific ? 0x01 : 0x00)); // Frame Control Field
buf.add8(transacId); // Transaction Sequance Number
buf.add8(cmdId);
buf.addBuffer(msg, len); // add the payload
ZigbeeZNPSend(buf.getBuffer(), buf.len());
}
inline int8_t hexValue(char c) {
if ((c >= '0') && (c <= '9')) {
return c - '0';
}
if ((c >= 'A') && (c <= 'F')) {
return 10 + c - 'A';
}
if ((c >= 'a') && (c <= 'f')) {
return 10 + c - 'a';
}
return -1;
}
uint32_t parseHex(const char **data, size_t max_len = 8) {
uint32_t ret = 0;
for (uint32_t i = 0; i < max_len; i++) {
int8_t v = hexValue(**data);
if (v < 0) { break; } // non hex digit, we stop parsing
ret = (ret << 4) | v;
*data += 1;
}
return ret;
}
void CmndZigbeeZCLSend(void) {
char parm_uc[12]; // used to convert JSON keys to uppercase
// ZigbeeZCLSend { "dst":"0x1234", "cluster":"0x0300", "endpoint":"0x01", "cmd":10, "data":"AABBCC" }
char dataBufUc[XdrvMailbox.data_len];
UpperCase(dataBufUc, XdrvMailbox.data);
RemoveSpace(dataBufUc);
if (strlen(dataBufUc) < 8) { ResponseCmndChar(D_JSON_INVALID_JSON); return; }
DynamicJsonBuffer jsonBuf;
JsonObject &json = jsonBuf.parseObject(dataBufUc);
if (!json.success()) { ResponseCmndChar(D_JSON_INVALID_JSON); return; }
// params
uint16_t dstAddr = 0xFFFF; // 0xFFFF is braodcast, so considered invalid
uint16_t clusterId = 0x0000; // 0x0000 is a valid default value
uint8_t endpoint = 0x00; // 0x00 is invalid for the dst endpoint
uint8_t cmd = ZCL_READ_ATTRIBUTES; // default command is READ_ATTRIBUTES
bool clusterSpecific = false;
const char* data = ""; // empty string is valid
UpperCase_P(parm_uc, PSTR("device"));
if (json.containsKey(parm_uc)) { dstAddr = strToUInt(json[parm_uc]); }
UpperCase_P(parm_uc, PSTR("endpoint"));
if (json.containsKey(parm_uc)) { endpoint = strToUInt(json[parm_uc]); }
UpperCase_P(parm_uc, PSTR("cmd"));
if (json.containsKey(parm_uc)) { data = json[parm_uc].as<const char*>(); }
// Parse 'cmd' in the form "AAAA_BB/CCCCCCCC" or "AAAA!BB/CCCCCCCC"
// where AA is the cluster number, BBBB the command number, CCCC... the payload
// First delimiter is '_' for a global command, or '!' for a cluster specific commanc
clusterId = parseHex(&data, 4);
// delimiter
if (('_' == *data) || ('!' == *data)) {
if ('!' == *data) { clusterSpecific = true; }
data++;
} else {
ResponseCmndChar("Wrong delimiter for payload");
return;
}
// parse cmd number
cmd = parseHex(&data, 2);
// move to end of payload
// delimiter is optional
if ('/' == *data) { data++; } // skip delimiter
size_t size = strlen(data);
SBuffer buf((size+2)/2); // actual bytes buffer for data
while (*data) {
uint8_t code = parseHex(&data, 2);
buf.add8(code);
}
if (0 == endpoint) {
// endpoint is not specified, let's try to find it from shortAddr
endpoint = zigbee_devices.findClusterEndpointIn(dstAddr, clusterId);
AddLog_P2(LOG_LEVEL_DEBUG, PSTR("CmndZigbeeZCLSend: guessing endpoint 0x%02X"), endpoint);
}
AddLog_P2(LOG_LEVEL_DEBUG, PSTR("CmndZigbeeZCLSend: dstAddr 0x%04X, cluster 0x%04X, endpoint 0x%02X, cmd 0x%02X, data %s"),
dstAddr, clusterId, endpoint, cmd, data);
if (0 == endpoint) {
AddLog_P2(LOG_LEVEL_INFO, PSTR("CmndZigbeeZCLSend: unspecified endpoint"));
return;
}
// everything is good, we can send the command
ZigbeeZCLSend(dstAddr, clusterId, endpoint, cmd, clusterSpecific, buf.getBuffer(), buf.len());
ResponseCmndDone();
}
// Probe a specific device to get its endpoints and supported clusters
void CmndZigbeeProbe(void) {
char dataBufUc[XdrvMailbox.data_len];
UpperCase(dataBufUc, XdrvMailbox.data);
RemoveSpace(dataBufUc);
if (strlen(dataBufUc) < 3) { ResponseCmndChar("Invalid destination"); return; }
// TODO, for now ignore friendly names
uint16_t shortaddr = strtoull(dataBufUc, nullptr, 0);
AddLog_P2(LOG_LEVEL_DEBUG, PSTR("CmndZigbeeScan: short addr 0x%04X"), shortaddr);
// everything is good, we can send the command
Z_SendActiveEpReq(shortaddr);
ResponseCmndDone();
}
// Allow or Deny pairing of new Zigbee devices