diff --git a/sonoff/_changelog.ino b/sonoff/_changelog.ino index b9ea7ff21..5cecae288 100644 --- a/sonoff/_changelog.ino +++ b/sonoff/_changelog.ino @@ -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) diff --git a/sonoff/i18n.h b/sonoff/i18n.h index d57c3f1a2..cee36cd89 100644 --- a/sonoff/i18n.h +++ b/sonoff/i18n.h @@ -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 diff --git a/sonoff/support_static_buffer.ino b/sonoff/support_static_buffer.ino index 38838c084..9d1cb1031 100644 --- a/sonoff/support_static_buffer.ino +++ b/sonoff/support_static_buffer.ino @@ -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++) { diff --git a/sonoff/xdrv_23_zigbee_0_constants.ino b/sonoff/xdrv_23_zigbee_0_constants.ino index 8ff4174eb..13d7dbb4f 100644 --- a/sonoff/xdrv_23_zigbee_0_constants.ino +++ b/sonoff/xdrv_23_zigbee_0_constants.ino @@ -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; diff --git a/sonoff/xdrv_23_zigbee_3_devices.ino b/sonoff/xdrv_23_zigbee_3_devices.ino index 78339b073..3b0989027 100644 --- a/sonoff/xdrv_23_zigbee_3_devices.ino +++ b/sonoff/xdrv_23_zigbee_3_devices.ino @@ -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 & 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 diff --git a/sonoff/xdrv_23_zigbee_5_converters.ino b/sonoff/xdrv_23_zigbee_5_converters.ino index 945322b61..8aa11ad01 100644 --- a/sonoff/xdrv_23_zigbee_5_converters.ino +++ b/sonoff/xdrv_23_zigbee_5_converters.ino @@ -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 diff --git a/sonoff/xdrv_23_zigbee_7_statemachine.ino b/sonoff/xdrv_23_zigbee_7_statemachine.ino index 385fb3980..7bd3a4129 100644 --- a/sonoff/xdrv_23_zigbee_7_statemachine.ino +++ b/sonoff/xdrv_23_zigbee_7_statemachine.ino @@ -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) diff --git a/sonoff/xdrv_23_zigbee_8_parsers.ino b/sonoff/xdrv_23_zigbee_8_parsers.ino index 6828cf96c..4050e45ae 100644 --- a/sonoff/xdrv_23_zigbee_8_parsers.ino +++ b/sonoff/xdrv_23_zigbee_8_parsers.ino @@ -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); diff --git a/sonoff/xdrv_23_zigbee_9_impl.ino b/sonoff/xdrv_23_zigbee_9_impl.ino index b41a2635b..69ae1d12b 100644 --- a/sonoff/xdrv_23_zigbee_9_impl.ino +++ b/sonoff/xdrv_23_zigbee_9_impl.ino @@ -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()) { + return val.as(); + } else { + if (val.is()) { + return strtoull(val.as(), 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(); } + + // 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