From 6a916e3b81568a6b4effde4f5c98a09e5ee32e33 Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Sun, 3 Nov 2019 12:41:44 +0100 Subject: [PATCH] Zigbee command support, considered as v1.0 for full Zigbee support --- tasmota/_changelog.ino | 1 + tasmota/i18n.h | 5 +- tasmota/xdrv_23_zigbee_3_devices.ino | 18 +- tasmota/xdrv_23_zigbee_5_converters.ino | 178 +++++++++++-------- tasmota/xdrv_23_zigbee_6_commands.ino | 41 ++++- tasmota/xdrv_23_zigbee_8_parsers.ino | 1 - tasmota/xdrv_23_zigbee_9_impl.ino | 221 +++++++++++++++++------- 7 files changed, 324 insertions(+), 141 deletions(-) diff --git a/tasmota/_changelog.ino b/tasmota/_changelog.ino index c0cc8fd88..cbee73d9e 100644 --- a/tasmota/_changelog.ino +++ b/tasmota/_changelog.ino @@ -4,6 +4,7 @@ * Add support for Honeywell I2C HIH series Humidity and Temperetaure sensor (#6808) * Fix wrong Dimmer behavior introduced with #6799 when SetOption37 < 128 * Change add DS18x20 support in Tasmota-IR + * Add Zigbee command support, considered as v1.0 for full Zigbee support * * 7.0.0.1 20191027 * Remove references to versions before 6.0 diff --git a/tasmota/i18n.h b/tasmota/i18n.h index bf73cc65b..fbfa3477f 100644 --- a/tasmota/i18n.h +++ b/tasmota/i18n.h @@ -456,6 +456,7 @@ #define D_JSON_TUYA_MCU_RECEIVED "TuyaReceived" // Commands xdrv_23_zigbee.ino +#define D_ZIGBEE_NOT_STARTED "Zigbee not started (yet)" #define D_CMND_ZIGBEE_PERMITJOIN "ZigbeePermitJoin" #define D_CMND_ZIGBEE_STATUS "ZigbeeStatus" #define D_CMND_ZIGBEE_RESET "ZigbeeReset" @@ -468,12 +469,12 @@ #define D_JSON_ZIGBEEZCL_RAW_RECEIVED "ZigbeeZCLRawReceived" #define D_JSON_ZIGBEE_DEVICE "Device" #define D_JSON_ZIGBEE_NAME "Name" -#define D_CMND_ZIGBEE_ZCL_SEND "ZigbeeZCLSend" - #define D_JSON_ZIGBEE_ZCL_SENT "ZigbeeZCLSent" #define D_CMND_ZIGBEE_PROBE "ZigbeeProbe" #define D_CMND_ZIGBEE_RECEIVED "ZigbeeReceived" #define D_CMND_ZIGBEE_LINKQUALITY "LinkQuality" #define D_CMND_ZIGBEE_READ "ZigbeeRead" +#define D_CMND_ZIGBEE_SEND "ZigbeeSend" + #define D_JSON_ZIGBEE_ZCL_SENT "ZigbeeZCLSent" // Commands xdrv_25_A4988_Stepper.ino #ifdef USE_A4988_STEPPER diff --git a/tasmota/xdrv_23_zigbee_3_devices.ino b/tasmota/xdrv_23_zigbee_3_devices.ino index 7068973c0..f76c882c7 100644 --- a/tasmota/xdrv_23_zigbee_3_devices.ino +++ b/tasmota/xdrv_23_zigbee_3_devices.ino @@ -68,7 +68,7 @@ public: void updateLastSeen(uint16_t shortaddr); // Dump json - String dump(uint8_t dump_mode) const; + String dump(uint32_t dump_mode, int32_t device_num = 0) const; private: std::vector _devices = {}; @@ -349,13 +349,21 @@ void Z_Devices::updateLastSeen(uint16_t shortaddr) { // 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 -String Z_Devices::dump(uint8_t dump_mode) const { +String Z_Devices::dump(uint32_t dump_mode, int32_t device_num) const { DynamicJsonBuffer jsonBuffer; JsonArray& json = jsonBuffer.createArray(); JsonArray& devices = json; //JsonArray& devices = json.createNestedArray(F("ZigbeeDevices")); - for (std::vector::const_iterator it = _devices.begin(); it != _devices.end(); ++it) { + // if device_num == 0, then we show all devices. + // When no payload, the default is -99. In this case change it to 0. + if (device_num < 0) { device_num = 0; } + + uint32_t device_current = 1; + for (std::vector::const_iterator it = _devices.begin(); it != _devices.end(); ++it, ++device_current) { + // ignore non-current device, if specified device is non-zero + if ((device_num > 0) && (device_num != device_current)) { continue; } + const Z_Device& device = *it; uint16_t shortaddr = device.shortaddr; char hex[20]; @@ -369,7 +377,7 @@ String Z_Devices::dump(uint8_t dump_mode) const { dev[F(D_JSON_ZIGBEE_NAME)] = device.friendlyName; } - if (1 == dump_mode) { + if (2 <= dump_mode) { Uint64toHex(device.longaddr, hex, 64); dev[F("IEEEAddr")] = hex; if (device.modelId.length() > 0) { @@ -381,7 +389,7 @@ String Z_Devices::dump(uint8_t dump_mode) const { } // If dump_mode == 2, dump a lot more details - if (2 == dump_mode) { + if (3 <= dump_mode) { JsonObject& dev_endpoints = dev.createNestedObject(F("Endpoints")); for (std::vector::const_iterator ite = device.endpoints.begin() ; ite != device.endpoints.end(); ++ite) { uint32_t ep_profile = *ite; diff --git a/tasmota/xdrv_23_zigbee_5_converters.ino b/tasmota/xdrv_23_zigbee_5_converters.ino index ec4a29280..57d4e3624 100644 --- a/tasmota/xdrv_23_zigbee_5_converters.ino +++ b/tasmota/xdrv_23_zigbee_5_converters.ino @@ -209,12 +209,19 @@ uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer } } break; - // Note: uint40, uint48, uint56, uint64 are not used in ZCL, so they are not implemented (yet) - case 0x24: // int40 - case 0x25: // int48 - case 0x26: // int56 - case 0x27: // int64 - i += attrtype - 0x1F; // 5 - 8; + // Note: uint40, uint48, uint56, uint64 are stored as Hex + case 0x24: // uint40 + case 0x25: // uint48 + case 0x26: // uint56 + case 0x27: // uint64 + { + uint8_t len = attrtype - 0x1F; // 5 - 8 + // print as HEX + char hex[2*len+1]; + ToHex_P(buf.buf(i), len, hex, sizeof(hex)); + json[attrid_str] = hex; + i += len; + } break; case 0x28: // uint8 { @@ -243,12 +250,19 @@ uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer } } break; - // Note: int40, int48, int56, int64 are not used in ZCL, so they are not implemented (yet) + // Note: int40, int48, int56, int64 are not stored as Hex case 0x2C: // int40 case 0x2D: // int48 case 0x2E: // int56 case 0x2F: // int64 - i += attrtype - 0x27; // 5 - 8; + { + uint8_t len = attrtype - 0x27; // 5 - 8 + // print as HEX + char hex[2*len+1]; + ToHex_P(buf.buf(i), len, hex, sizeof(hex)); + json[attrid_str] = hex; + i += len; + } break; case 0x41: // octet string, 1 byte len @@ -452,10 +466,6 @@ typedef struct Z_AttributeConverter { // list of post-processing directives const Z_AttributeConverter Z_PostProcess[] PROGMEM = { - // { 0x0000, 0x0004, "Manufacturer", &Z_ManufKeep }, // record Manufacturer - // { 0x0000, 0x0005, D_JSON_MODEL D_JSON_ID, &Z_ModelKeep }, // record Model - // { 0x0405, 0x0000, D_JSON_HUMIDITY, &Z_FloatDiv100 }, // Humidity - { 0x0000, 0x0000, "ZCLVersion", &Z_Copy }, { 0x0000, 0x0001, "AppVersion", &Z_Copy }, { 0x0000, 0x0002, "StackVersion", &Z_Copy }, @@ -466,51 +476,36 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { { 0x0000, 0x0007, "PowerSource", &Z_Copy }, { 0x0000, 0x4000, "SWBuildID", &Z_Copy }, { 0x0000, 0xFFFF, nullptr, &Z_Remove }, // Remove all other values + // Cmd 0x0A - Cluster 0x0000, attribute 0xFF01 - proprietary + { 0x0000, 0xFF01, nullptr, &Z_AqaraSensor }, // Occupancy (map8) - // Color Control cluster - { 0x0003, 0x0000, "CurrentHue", &Z_Copy }, - { 0x0003, 0x0001, "CurrentSaturation", &Z_Copy }, - { 0x0003, 0x0002, "RemainingTime", &Z_Copy }, - { 0x0003, 0x0003, "CurrentX", &Z_Copy }, - { 0x0003, 0x0004, "CurrentY", &Z_Copy }, - { 0x0003, 0x0005, "DriftCompensation", &Z_Copy }, - { 0x0003, 0x0006, "CompensationText", &Z_Copy }, - { 0x0003, 0x0007, "ColorTemperatureMireds",&Z_Copy }, - { 0x0003, 0x0008, "ColorMode", &Z_Copy }, - { 0x0003, 0x0010, "NumberOfPrimaries", &Z_Copy }, - { 0x0003, 0x0011, "Primary1X", &Z_Copy }, - { 0x0003, 0x0012, "Primary1Y", &Z_Copy }, - { 0x0003, 0x0013, "Primary1Intensity", &Z_Copy }, - { 0x0003, 0x0015, "Primary2X", &Z_Copy }, - { 0x0003, 0x0016, "Primary2Y", &Z_Copy }, - { 0x0003, 0x0017, "Primary2Intensity", &Z_Copy }, - { 0x0003, 0x0019, "Primary3X", &Z_Copy }, - { 0x0003, 0x001A, "Primary3Y", &Z_Copy }, - { 0x0003, 0x001B, "Primary3Intensity", &Z_Copy }, - { 0x0003, 0x0030, "WhitePointX", &Z_Copy }, - { 0x0003, 0x0031, "WhitePointY", &Z_Copy }, - { 0x0003, 0x0032, "ColorPointRX", &Z_Copy }, - { 0x0003, 0x0033, "ColorPointRY", &Z_Copy }, - { 0x0003, 0x0034, "ColorPointRIntensity", &Z_Copy }, - { 0x0003, 0x0036, "ColorPointGX", &Z_Copy }, - { 0x0003, 0x0037, "ColorPointGY", &Z_Copy }, - { 0x0003, 0x0038, "ColorPointGIntensity", &Z_Copy }, - { 0x0003, 0x003A, "ColorPointBX", &Z_Copy }, - { 0x0003, 0x003B, "ColorPointBY", &Z_Copy }, - { 0x0003, 0x003C, "ColorPointBIntensity", &Z_Copy }, + // Power Configuration cluster + { 0x0001, 0x0000, "MainsVoltage", &Z_Copy }, + { 0x0001, 0x0001, "MainsFrequency", &Z_Copy }, + { 0x0001, 0x0020, "BatteryVoltage", &Z_Copy }, + { 0x0001, 0x0021, "BatteryPercentageRemaining",&Z_Copy }, + + // Device Temperature Configuration cluster + { 0x0002, 0x0000, "CurrentTemperature", &Z_Copy }, + { 0x0002, 0x0001, "MinTempExperienced", &Z_Copy }, + { 0x0002, 0x0002, "MaxTempExperienced", &Z_Copy }, + { 0x0002, 0x0003, "OverTempTotalDwell", &Z_Copy }, // On/off cluster { 0x0006, 0x0000, "Power", &Z_Copy }, + // On/Off Switch Configuration cluster { 0x0007, 0x0000, "SwitchType", &Z_Copy }, + // Level Control cluster { 0x0008, 0x0000, "CurrentLevel", &Z_Copy }, - { 0x0008, 0x0001, "RemainingTime", &Z_Copy }, - { 0x0008, 0x0010, "OnOffTransitionTime", &Z_Copy }, - { 0x0008, 0x0011, "OnLevel", &Z_Copy }, - { 0x0008, 0x0012, "OnTransitionTime", &Z_Copy }, - { 0x0008, 0x0013, "OffTransitionTime", &Z_Copy }, - { 0x0008, 0x0014, "DefaultMoveRate", &Z_Copy }, + // { 0x0008, 0x0001, "RemainingTime", &Z_Copy }, + // { 0x0008, 0x0010, "OnOffTransitionTime", &Z_Copy }, + // { 0x0008, 0x0011, "OnLevel", &Z_Copy }, + // { 0x0008, 0x0012, "OnTransitionTime", &Z_Copy }, + // { 0x0008, 0x0013, "OffTransitionTime", &Z_Copy }, + // { 0x0008, 0x0014, "DefaultMoveRate", &Z_Copy }, + // Alarms cluster { 0x0009, 0x0000, "AlarmCount", &Z_Copy }, // Time cluster @@ -602,11 +597,12 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { { 0x0014, 0x0068, "RelinquishDefault", &Z_Copy }, { 0x0014, 0x006F, "StatusFlags", &Z_Copy }, { 0x0014, 0x0100, "ApplicationType", &Z_Copy }, - // Diagnostics cluster - { 0x0B05, 0x0000, "NumberOfResets", &Z_Copy }, - { 0x0B05, 0x0001, "PersistentMemoryWrites",&Z_Copy }, - { 0x0B05, 0x011C, "LastMessageLQI", &Z_Copy }, - { 0x0B05, 0x011D, "LastMessageRSSI", &Z_Copy }, + // Power Profile cluster + { 0x001A, 0x0000, "TotalProfileNum", &Z_Copy }, + { 0x001A, 0x0001, "MultipleScheduling", &Z_Copy }, + { 0x001A, 0x0002, "EnergyFormatting", &Z_Copy }, + { 0x001A, 0x0003, "EnergyRemote", &Z_Copy }, + { 0x001A, 0x0004, "ScheduleMode", &Z_Copy }, // Poll Control cluster { 0x0020, 0x0000, "CheckinInterval", &Z_Copy }, { 0x0020, 0x0001, "LongPollInterval", &Z_Copy }, @@ -651,24 +647,39 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { { 0x0102, 0x0018, "IntermediateSetpointsLift",&Z_Copy }, { 0x0102, 0x0019, "IntermediateSetpointsTilt",&Z_Copy }, - // Power Profile cluster - { 0x001A, 0x0000, "TotalProfileNum", &Z_Copy }, - { 0x001A, 0x0001, "MultipleScheduling", &Z_Copy }, - { 0x001A, 0x0002, "EnergyFormatting", &Z_Copy }, - { 0x001A, 0x0003, "EnergyRemote", &Z_Copy }, - { 0x001A, 0x0004, "ScheduleMode", &Z_Copy }, - // Meter Identification cluster - { 0x0B01, 0x0000, "CompanyName", &Z_Copy }, - { 0x0B01, 0x0001, "MeterTypeID", &Z_Copy }, - { 0x0B01, 0x0004, "DataQualityID", &Z_Copy }, - { 0x0B01, 0x0005, "CustomerName", &Z_Copy }, - { 0x0B01, 0x0006, "Model", &Z_Copy }, - { 0x0B01, 0x0007, "PartNumber", &Z_Copy }, - { 0x0B01, 0x000A, "SoftwareRevision", &Z_Copy }, - { 0x0B01, 0x000C, "POD", &Z_Copy }, - { 0x0B01, 0x000D, "AvailablePower", &Z_Copy }, - { 0x0B01, 0x000E, "PowerThreshold", &Z_Copy }, + // Color Control cluster + { 0x0300, 0x0000, "CurrentHue", &Z_Copy }, + { 0x0300, 0x0001, "CurrentSaturation", &Z_Copy }, + { 0x0300, 0x0002, "RemainingTime", &Z_Copy }, + { 0x0300, 0x0003, "CurrentX", &Z_Copy }, + { 0x0300, 0x0004, "CurrentY", &Z_Copy }, + { 0x0300, 0x0005, "DriftCompensation", &Z_Copy }, + { 0x0300, 0x0006, "CompensationText", &Z_Copy }, + { 0x0300, 0x0007, "ColorTemperatureMireds",&Z_Copy }, + { 0x0300, 0x0008, "ColorMode", &Z_Copy }, + { 0x0300, 0x0010, "NumberOfPrimaries", &Z_Copy }, + { 0x0300, 0x0011, "Primary1X", &Z_Copy }, + { 0x0300, 0x0012, "Primary1Y", &Z_Copy }, + { 0x0300, 0x0013, "Primary1Intensity", &Z_Copy }, + { 0x0300, 0x0015, "Primary2X", &Z_Copy }, + { 0x0300, 0x0016, "Primary2Y", &Z_Copy }, + { 0x0300, 0x0017, "Primary2Intensity", &Z_Copy }, + { 0x0300, 0x0019, "Primary3X", &Z_Copy }, + { 0x0300, 0x001A, "Primary3Y", &Z_Copy }, + { 0x0300, 0x001B, "Primary3Intensity", &Z_Copy }, + { 0x0300, 0x0030, "WhitePointX", &Z_Copy }, + { 0x0300, 0x0031, "WhitePointY", &Z_Copy }, + { 0x0300, 0x0032, "ColorPointRX", &Z_Copy }, + { 0x0300, 0x0033, "ColorPointRY", &Z_Copy }, + { 0x0300, 0x0034, "ColorPointRIntensity", &Z_Copy }, + { 0x0300, 0x0036, "ColorPointGX", &Z_Copy }, + { 0x0300, 0x0037, "ColorPointGY", &Z_Copy }, + { 0x0300, 0x0038, "ColorPointGIntensity", &Z_Copy }, + { 0x0300, 0x003A, "ColorPointBX", &Z_Copy }, + { 0x0300, 0x003B, "ColorPointBY", &Z_Copy }, + { 0x0300, 0x003C, "ColorPointBIntensity", &Z_Copy }, + // Illuminance Measurement cluster { 0x0400, 0x0000, D_JSON_ILLUMINANCE, &Z_Copy }, // Illuminance (in Lux) { 0x0400, 0x0001, "MinMeasuredValue", &Z_Copy }, // { 0x0400, 0x0002, "MaxMeasuredValue", &Z_Copy }, // @@ -676,16 +687,19 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { { 0x0400, 0x0004, "LightSensorType", &Z_Copy }, // { 0x0400, 0xFFFF, nullptr, &Z_Remove }, // Remove all other values + // Illuminance Level Sensing cluster { 0x0401, 0x0000, "LevelStatus", &Z_Copy }, // Illuminance (in Lux) { 0x0401, 0x0001, "LightSensorType", &Z_Copy }, // LightSensorType { 0x0401, 0xFFFF, nullptr, &Z_Remove }, // Remove all other values + // Temperature Measurement cluster { 0x0402, 0x0000, D_JSON_TEMPERATURE, &Z_FloatDiv100 }, // Temperature { 0x0402, 0x0001, "MinMeasuredValue", &Z_FloatDiv100 }, // { 0x0402, 0x0002, "MaxMeasuredValue", &Z_FloatDiv100 }, // { 0x0402, 0x0003, "Tolerance", &Z_FloatDiv100 }, // { 0x0402, 0xFFFF, nullptr, &Z_Remove }, // Remove all other values + // Pressure Measurement cluster { 0x0403, 0x0000, D_JSON_PRESSURE_UNIT, &Z_AddPressureUnit }, // Pressure Unit { 0x0403, 0x0000, D_JSON_PRESSURE, &Z_Copy }, // Pressure { 0x0403, 0x0001, "MinMeasuredValue", &Z_Copy }, // @@ -698,24 +712,42 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { { 0x0403, 0x0014, "Scale", &Z_Copy }, // { 0x0403, 0xFFFF, nullptr, &Z_Remove }, // Remove all other Pressure values + // Flow Measurement cluster { 0x0404, 0x0000, D_JSON_FLOWRATE, &Z_FloatDiv10 }, // Flow (in m3/h) { 0x0404, 0x0001, "MinMeasuredValue", &Z_Copy }, // { 0x0404, 0x0002, "MaxMeasuredValue", &Z_Copy }, // { 0x0404, 0x0003, "Tolerance", &Z_Copy }, // { 0x0404, 0xFFFF, nullptr, &Z_Remove }, // Remove all other values + // Relative Humidity Measurement cluster { 0x0405, 0x0000, D_JSON_HUMIDITY, &Z_FloatDiv100 }, // Humidity { 0x0405, 0x0001, "MinMeasuredValue", &Z_Copy }, // { 0x0405, 0x0002, "MaxMeasuredValue", &Z_Copy }, // { 0x0405, 0x0003, "Tolerance", &Z_Copy }, // { 0x0405, 0xFFFF, nullptr, &Z_Remove }, // Remove all other values + // Occupancy Sensing cluster { 0x0406, 0x0000, "Occupancy", &Z_Copy }, // Occupancy (map8) { 0x0406, 0x0001, "OccupancySensorType", &Z_Copy }, // OccupancySensorType { 0x0406, 0xFFFF, nullptr, &Z_Remove }, // Remove all other values - // Cmd 0x0A - Cluster 0x0000, attribute 0xFF01 - proprietary - { 0x0000, 0xFF01, nullptr, &Z_AqaraSensor }, // Occupancy (map8) + // Meter Identification cluster + { 0x0B01, 0x0000, "CompanyName", &Z_Copy }, + { 0x0B01, 0x0001, "MeterTypeID", &Z_Copy }, + { 0x0B01, 0x0004, "DataQualityID", &Z_Copy }, + { 0x0B01, 0x0005, "CustomerName", &Z_Copy }, + { 0x0B01, 0x0006, "Model", &Z_Copy }, + { 0x0B01, 0x0007, "PartNumber", &Z_Copy }, + { 0x0B01, 0x000A, "SoftwareRevision", &Z_Copy }, + { 0x0B01, 0x000C, "POD", &Z_Copy }, + { 0x0B01, 0x000D, "AvailablePower", &Z_Copy }, + { 0x0B01, 0x000E, "PowerThreshold", &Z_Copy }, + + // Diagnostics cluster + { 0x0B05, 0x0000, "NumberOfResets", &Z_Copy }, + { 0x0B05, 0x0001, "PersistentMemoryWrites",&Z_Copy }, + { 0x0B05, 0x011C, "LastMessageLQI", &Z_Copy }, + { 0x0B05, 0x011D, "LastMessageRSSI", &Z_Copy }, }; diff --git a/tasmota/xdrv_23_zigbee_6_commands.ino b/tasmota/xdrv_23_zigbee_6_commands.ino index 1dcbe3610..c877bd3a3 100644 --- a/tasmota/xdrv_23_zigbee_6_commands.ino +++ b/tasmota/xdrv_23_zigbee_6_commands.ino @@ -31,11 +31,15 @@ const Z_CommandConverter Z_Commands[] = { { "Dimmer", "0008!04/xx0A00" }, // Move to Level with On/Off, xx=0..254 (255 is invalid) { "Dimmer+", "0008!06/001902" }, // Step up by 10%, 0.2 secs { "Dimmer-", "0008!06/011902" }, // Step down by 10%, 0.2 secs + { "DimmerStop", "0008!03" }, // Stop any Dimmer animation + { "ResetAlarm", "0009!00/xxyyyy" }, // Reset alarm (alarm code + cluster identifier) + { "ResetAllAlarms","0009!01" }, // Reset all alarms { "Hue", "0300!00/xx000A00" }, // Move to Hue, shortest time, 1s { "Sat", "0300!03/xx0A00" }, // Move to Sat { "HueSat", "0300!06/xxyy0A00" }, // Hue, Sat { "Color", "0300!07/xxxxyyyy0A00" }, // x, y (uint16) { "CT", "0300!0A/xxxx0A00" }, // Color Temperature Mireds (uint16) + { "Shutter", "0102!xx" }, { "ShutterOpen", "0102!00"}, { "ShutterClose", "0102!01"}, { "ShutterStop", "0102!02"}, @@ -43,6 +47,18 @@ const Z_CommandConverter Z_Commands[] = { { "ShutterTilt", "0102!08xx"}, // Tilt percentage }; +const __FlashStringHelper* zigbeeFindCommand(const char *command) { + char parm_uc[16]; // used to convert JSON keys to uppercase + for (uint32_t i = 0; i < sizeof(Z_Commands) / sizeof(Z_Commands[0]); i++) { + const Z_CommandConverter *conv = &Z_Commands[i]; + if (0 == strcasecmp_P(command, conv->tasmota_cmd)) { + return (const __FlashStringHelper*) conv->zcl_cmd; + } + } + + return nullptr; +} + inline bool isXYZ(char c) { return (c >= 'x') && (c <= 'z'); } @@ -54,8 +70,7 @@ inline char hexDigit(uint32_t h) { } // replace all xx/yy/zz substrings with unsigned ints, and the corresponding len (8, 16 or 32 bits) -// zcl_cmd can be in PROGMEM -String SendZCLCommand_P(const char *zcl_cmd_P, uint32_t x, uint32_t y, uint32_t z) { +String zigbeeCmdAddParams(const char *zcl_cmd_P, uint32_t x, uint32_t y, uint32_t z) { size_t len = strlen_P(zcl_cmd_P); char zcl_cmd[len+1]; strcpy_P(zcl_cmd, zcl_cmd_P); // copy into RAM @@ -89,4 +104,26 @@ String SendZCLCommand_P(const char *zcl_cmd_P, uint32_t x, uint32_t y, uint32_t return String(zcl_cmd); } +const char kZ_Alias[] PROGMEM = "OFF|" D_OFF "|" D_FALSE "|" D_STOP "|" "OPEN" "|" // 0 + "ON|" D_ON "|" D_TRUE "|" D_START "|" "CLOSE" "|" // 1 + "TOGGLE|" D_TOGGLE "|" // 2 + "ALL" ; // 255 + +const uint8_t kZ_Numbers[] PROGMEM = { 0,0,0,0,0, + 1,1,1,1,1, + 2,2, + 255 }; + +uint32_t ZigbeeAliasOrNumber(const char *state_text) { + char command[16]; + int state_number = GetCommandCode(command, sizeof(command), state_text, kZ_Alias); + if (state_number >= 0) { + // found an alias, get its value + return pgm_read_byte(kZ_Numbers + state_number); + } else { + // no alias found, convert it as number + return strtoul(state_text, nullptr, 0); + } +} + #endif // USE_ZIGBEE diff --git a/tasmota/xdrv_23_zigbee_8_parsers.ino b/tasmota/xdrv_23_zigbee_8_parsers.ino index d0d7a3079..e5a0670be 100644 --- a/tasmota/xdrv_23_zigbee_8_parsers.ino +++ b/tasmota/xdrv_23_zigbee_8_parsers.ino @@ -325,7 +325,6 @@ 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); if (cluster) { Z_SendAFInfoRequest(nwkAddr, cluster, 0x0000, 0x01); // TODO, do we need tarnsacId counter? } diff --git a/tasmota/xdrv_23_zigbee_9_impl.ino b/tasmota/xdrv_23_zigbee_9_impl.ino index a935ba08a..a8736003c 100644 --- a/tasmota/xdrv_23_zigbee_9_impl.ino +++ b/tasmota/xdrv_23_zigbee_9_impl.ino @@ -37,11 +37,11 @@ TasmotaSerial *ZigbeeSerial = nullptr; const char kZigbeeCommands[] PROGMEM = "|" D_CMND_ZIGBEEZNPSEND "|" D_CMND_ZIGBEE_PERMITJOIN - "|" D_CMND_ZIGBEE_STATUS "|" D_CMND_ZIGBEE_RESET "|" D_CMND_ZIGBEE_ZCL_SEND - "|" D_CMND_ZIGBEE_PROBE "|" D_CMND_ZIGBEE_READ; + "|" D_CMND_ZIGBEE_STATUS "|" D_CMND_ZIGBEE_RESET "|" D_CMND_ZIGBEE_SEND + "|" D_CMND_ZIGBEE_PROBE "|" D_CMND_ZIGBEE_READ ; void (* const ZigbeeCommand[])(void) PROGMEM = { &CmndZigbeeZNPSend, &CmndZigbeePermitJoin, - &CmndZigbeeStatus, &CmndZigbeeReset, &CmndZigbeeZCLSend, + &CmndZigbeeStatus, &CmndZigbeeReset, &CmndZigbeeSend, &CmndZigbeeProbe, &CmndZigbeeRead }; int32_t ZigbeeProcessInput(class SBuffer &buf) { @@ -246,7 +246,7 @@ void ZigbeeInit(void) * Commands \*********************************************************************************************/ -uint32_t strToUInt(const JsonVariant &val) { +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(); @@ -258,7 +258,9 @@ uint32_t strToUInt(const JsonVariant &val) { 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 +const unsigned char ZIGBEE_FACTORY_RESET[] PROGMEM = + { Z_SREQ | Z_SAPI, SAPI_WRITE_CONFIGURATION, CONF_STARTUP_OPTION, 0x01 /* len */, 0x01 /* STARTOPT_CLEAR_CONFIG */}; +//"2605030101"; // Z_SREQ | Z_SAPI, SAPI_WRITE_CONFIGURATION, CONF_STARTUP_OPTION, 0x01 len, 0x01 STARTOPT_CLEAR_CONFIG // Do a factory reset of the CC2530 void CmndZigbeeReset(void) { if (ZigbeeSerial) { @@ -276,7 +278,7 @@ void CmndZigbeeReset(void) { void CmndZigbeeStatus(void) { if (ZigbeeSerial) { - String dump = zigbee_devices.dump(XdrvMailbox.payload); + String dump = zigbee_devices.dump(XdrvMailbox.index, XdrvMailbox.payload); Response_P(PSTR("{\"%s%d\":%s}"), XdrvMailbox.command, XdrvMailbox.payload, dump.c_str()); } } @@ -385,37 +387,15 @@ uint32_t parseHex(const char **data, size_t max_len = 8) { return ret; } -void CmndZigbeeZCLSend(void) { - char parm_uc[12]; // used to convert JSON keys to uppercase - // ZigbeeZCLSend { "dst":"0x1234", "endpoint":"0x01", "cmd":"AABBCC" } - char dataBufUc[XdrvMailbox.data_len]; - UpperCase(dataBufUc, XdrvMailbox.data); - RemoveSpace(dataBufUc); - if (strlen(dataBufUc) < 8) { ResponseCmndChar(D_JSON_INVALID_JSON); return; } +void zigbeeZCLSendStr(uint16_t dstAddr, uint8_t endpoint, const char *data) { - 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 + uint16_t cluster = 0x0000; // 0x0000 is a valid default value 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); + cluster = parseHex(&data, 4); // delimiter if (('_' == *data) || ('!' == *data)) { @@ -442,24 +422,149 @@ void CmndZigbeeZCLSend(void) { 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); + endpoint = zigbee_devices.findClusterEndpointIn(dstAddr, cluster); + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZigbeeSend: 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); + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZigbeeSend: dstAddr 0x%04X, cluster 0x%04X, endpoint 0x%02X, cmd 0x%02X, data %s"), + dstAddr, cluster, endpoint, cmd, data); if (0 == endpoint) { - AddLog_P2(LOG_LEVEL_INFO, PSTR("CmndZigbeeZCLSend: unspecified endpoint")); + AddLog_P2(LOG_LEVEL_INFO, PSTR("ZigbeeSend: unspecified endpoint")); return; } // everything is good, we can send the command - ZigbeeZCLSend(dstAddr, clusterId, endpoint, cmd, clusterSpecific, buf.getBuffer(), buf.len()); + ZigbeeZCLSend(dstAddr, cluster, endpoint, cmd, clusterSpecific, buf.getBuffer(), buf.len()); ResponseCmndDone(); } +// Get an JSON attribute, with case insensitive key search +JsonVariant &getCaseInsensitive(const JsonObject &json, const char *needle) { + // key can be in PROGMEM + if ((nullptr == &json) || (nullptr == needle) || (0 == pgm_read_byte(needle))) { + return *(JsonVariant*)nullptr; + } + + for (auto kv : json) { + const char *key = kv.key; + JsonVariant &value = kv.value; + + if (0 == strcasecmp_P(key, needle)) { + return value; + } + } + // if not found + return *(JsonVariant*)nullptr; +} + +void CmndZigbeeSend(void) { + // ZigbeeSend { "device":"0x1234", "endpoint":"0x03", "send":{"Power":1} } + // ZigbeeSend { "device":"0x1234", "endpoint":"0x03", "send":{"Power":"3"} } + // ZigbeeSend { "device":"0x1234", "endpoint":"0x03", "send":{"Power":"0xFF"} } + // ZigbeeSend { "device":"0x1234", "endpoint":"0x03", "send":{"Power":null} } + // ZigbeeSend { "device":"0x1234", "endpoint":"0x03", "send":{"Power":false} } + // ZigbeeSend { "device":"0x1234", "endpoint":"0x03", "send":{"Power":true} } + // ZigbeeSend { "device":"0x1234", "endpoint":"0x03", "send":{"Power":"true"} } + // ZigbeeSend { "device":"0x1234", "endpoint":"0x03", "send":{"ShutterClose":null} } + // ZigbeeSend { "devicse":"0x1234", "endpoint":"0x03", "send":{"Power":1} } + // ZigbeeSend { "device":"0x1234", "endpoint":"0x03", "send":{"Color":"1,2"} } + // ZigbeeSend { "device":"0x1234", "endpoint":"0x03", "send":{"Color":"0x1122,0xFFEE"} } + if (zigbee.init_phase) { ResponseCmndChar(D_ZIGBEE_NOT_STARTED); return; } + DynamicJsonBuffer jsonBuf; + JsonObject &json = jsonBuf.parseObject(XdrvMailbox.data); + if (!json.success()) { ResponseCmndChar(D_JSON_INVALID_JSON); return; } + + // params + static char delim[] = ", "; // delimiters for parameters + uint16_t device = 0xFFFF; // 0xFFFF is broadcast, so considered valid + uint8_t endpoint = 0x00; // 0x00 is invalid for the dst endpoint + String cmd_str = ""; // the actual low-level command, either specified or computed + + const JsonVariant &val_device = getCaseInsensitive(json, PSTR("device")); + if (nullptr != &val_device) { device = strToUInt(val_device); } + const JsonVariant &val_endpoint = getCaseInsensitive(json, PSTR("endpoint")); + if (nullptr != &val_endpoint) { endpoint = strToUInt(val_endpoint); } + const JsonVariant val_cmd = getCaseInsensitive(json, PSTR("Send")); + if (nullptr != &val_cmd) { + // probe the type of the argument + // If JSON object, it's high level commands + // If String, it's a low level command + if (val_cmd.is()) { + // we have a high-level command + JsonObject &cmd_obj = val_cmd.as(); + int32_t cmd_size = cmd_obj.size(); + if (cmd_size > 1) { + Response_P(PSTR("Only 1 command allowed (%d)"), cmd_size); + return; + } else if (1 == cmd_size) { + // We have exactly 1 command, parse it + JsonObject::iterator it = cmd_obj.begin(); // just get the first key/value + String key = it->key; + JsonVariant& value = it->value; + uint32_t x = 0, y = 0, z = 0; + + const __FlashStringHelper* tasmota_cmd = zigbeeFindCommand(key.c_str()); + if (tasmota_cmd) { + cmd_str = tasmota_cmd; + } else { + Response_P(PSTR("Unrecognized zigbee command: %s"), key.c_str()); + return; + } + + // parse the JSON value, depending on its type fill in x,y,z + if (value.is()) { + x = value.as() ? 1 : 0; + } else if (value.is()) { + x = value.as(); + } else { + // if non-bool or non-int, trying char* + const char *s_const = value.as(); + if (s_const != nullptr) { + char s[strlen(s_const)+1]; + strcpy(s, s_const); + if ((nullptr != s) && (0x00 != *s)) { // ignore any null or empty string, could represent 'null' json value + char *sval = strtok(s, delim); + if (sval) { + x = ZigbeeAliasOrNumber(sval); + sval = strtok(nullptr, delim); + if (sval) { + y = ZigbeeAliasOrNumber(sval); + sval = strtok(nullptr, delim); + if (sval) { + z = ZigbeeAliasOrNumber(sval); + } + } + } + } + } + } + + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZigbeeSend: command_template = %s"), cmd_str.c_str()); + cmd_str = zigbeeCmdAddParams(cmd_str.c_str(), x, y, z); // fill in parameters + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZigbeeSend: command_final = %s"), cmd_str.c_str()); + } else { + // we have zero command, pass through until last error for missing command + } + } else if (val_cmd.is()) { + // low-level command + cmd_str = val_cmd.as(); + } else { + // we have an unsupported command type, just ignore it and fallback to missing command + } + + AddLog_P2(LOG_LEVEL_INFO, PSTR("ZigbeeCmd_actual: ZigbeeZCLSend {\"device\":\"0x%04X\",\"endpoint\":%d,\"send\":\"%s\"}"), + device, endpoint, cmd_str.c_str()); + zigbeeZCLSendStr(device, endpoint, cmd_str.c_str()); + } else { + Response_P(PSTR("Missing zigbee 'Send'")); + return; + } + +} + // Probe a specific device to get its endpoints and supported clusters void CmndZigbeeProbe(void) { + if (zigbee.init_phase) { ResponseCmndChar(D_ZIGBEE_NOT_STARTED); return; } char dataBufUc[XdrvMailbox.data_len]; UpperCase(dataBufUc, XdrvMailbox.data); RemoveSpace(dataBufUc); @@ -476,34 +581,33 @@ void CmndZigbeeProbe(void) { // Send an attribute read command to a device, specifying cluster and list of attributes void CmndZigbeeRead(void) { - char parm_uc[12]; // used to convert JSON keys to uppercase // ZigbeeRead {"Device":"0xF289","Cluster":0,"Endpoint":3,"Attr":5} // ZigbeeRead {"Device":"0xF289","Cluster":"0x0000","Endpoint":"0x0003","Attr":"0x0005"} // ZigbeeRead {"Device":"0xF289","Cluster":0,"Endpoint":3,"Attr":[5,6,7,4]} - char dataBufUc[XdrvMailbox.data_len]; - UpperCase(dataBufUc, XdrvMailbox.data); - RemoveSpace(dataBufUc); - if (strlen(dataBufUc) < 8) { ResponseCmndChar(D_JSON_INVALID_JSON); return; } - + if (zigbee.init_phase) { ResponseCmndChar(D_ZIGBEE_NOT_STARTED); return; } DynamicJsonBuffer jsonBuf; - JsonObject &json = jsonBuf.parseObject(dataBufUc); + JsonObject &json = jsonBuf.parseObject(XdrvMailbox.data); if (!json.success()) { ResponseCmndChar(D_JSON_INVALID_JSON); return; } // params - uint16_t dstAddr = 0x0000; // default to local address - uint16_t cluster = 0x0000; // default to general clsuter + uint16_t device = 0xFFFF; // 0xFFFF is braodcast, so considered valid + uint16_t cluster = 0x0000; // default to general cluster uint8_t endpoint = 0x00; // 0x00 is invalid for the dst endpoint - size_t attrs_len = 0; - uint8_t* attrs = nullptr; // empty string is valid + size_t attrs_len = 0; + uint8_t* attrs = nullptr; // empty string is valid - dstAddr = strToUInt(json[UpperCase_P(parm_uc, PSTR("device"))]); - endpoint = strToUInt(json[UpperCase_P(parm_uc, PSTR("endpoint"))]); - cluster = strToUInt(json[UpperCase_P(parm_uc, PSTR("cluster"))]); - UpperCase_P(parm_uc, PSTR("Attr")); - if (json.containsKey(parm_uc)) { - const JsonVariant& attr_data = json[parm_uc]; - if (attr_data.is()) { - JsonArray& attr_arr = attr_data; + + const JsonVariant &val_device = getCaseInsensitive(json, PSTR("Device")); + if (nullptr != &val_device) { device = strToUInt(val_device); } + const JsonVariant val_cluster = getCaseInsensitive(json, PSTR("Cluster")); + if (nullptr != &val_cluster) { cluster = strToUInt(val_cluster); } + const JsonVariant &val_endpoint = getCaseInsensitive(json, PSTR("Endpoint")); + if (nullptr != &val_endpoint) { endpoint = strToUInt(val_endpoint); } + + const JsonVariant &val_attr = getCaseInsensitive(json, PSTR("Read")); + if (nullptr != &val_attr) { + if (val_attr.is()) { + JsonArray& attr_arr = val_attr; attrs_len = attr_arr.size() * 2; attrs = new uint8_t[attrs_len]; @@ -517,13 +621,13 @@ void CmndZigbeeRead(void) { } else { attrs_len = 2; attrs = new uint8_t[attrs_len]; - uint16_t val = strToUInt(attr_data); + uint16_t val = strToUInt(val_attr); attrs[0] = val & 0xFF; // little endian attrs[1] = val >> 8; } } - ZigbeeZCLSend(dstAddr, cluster, endpoint, ZCL_READ_ATTRIBUTES, false, attrs, attrs_len, false /* we do want a response */); + ZigbeeZCLSend(device, cluster, endpoint, ZCL_READ_ATTRIBUTES, false, attrs, attrs_len, false /* we do want a response */); if (attrs) { delete[] attrs; } } @@ -531,6 +635,7 @@ void CmndZigbeeRead(void) { // Allow or Deny pairing of new Zigbee devices void CmndZigbeePermitJoin(void) { + if (zigbee.init_phase) { ResponseCmndChar(D_ZIGBEE_NOT_STARTED); return; } uint32_t payload = XdrvMailbox.payload; if (payload < 0) { payload = 0; } if ((99 != payload) && (payload > 1)) { payload = 1; }