mirror of https://github.com/arendst/Tasmota.git
Add Zigbee additional commands and sending messages to control devices (#6095)
This commit is contained in:
parent
36371fc3a7
commit
0092bf766c
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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++) {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue