From ff07d0608b9e920b18fa8db970fa42b9d988de38 Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Thu, 18 Aug 2022 19:23:11 +0200 Subject: [PATCH] Zigbee device plugin mechanism with commands ``ZbLoad``, ``ZbUnload`` and ``ZbLoadDump`` --- CHANGELOG.md | 1 + tasmota/include/i18n.h | 3 + ...s.ino => xdrv_23_zigbee_5_0_constants.ino} | 304 +++--- .../xdrv_23_zigbee_5_1_attributes.ino | 955 ++++++++++++++++++ ....ino => xdrv_23_zigbee_5_2_converters.ino} | 898 +++------------- .../xdrv_23_zigbee_6_commands.ino | 1 - .../xdrv_23_zigbee_7_0_statemachine.ino | 2 + .../xdrv_23_zigbee_7_6_plugin.ino | 379 +++++++ .../xdrv_23_zigbee_8_parsers.ino | 36 +- .../xdrv_23_zigbee_A_impl.ino | 110 +- tasmota/zigbee/giex_water.zb | 14 + 11 files changed, 1707 insertions(+), 996 deletions(-) rename tasmota/tasmota_xdrv_driver/{xdrv_23_zigbee_5__constants.ino => xdrv_23_zigbee_5_0_constants.ino} (82%) create mode 100644 tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_5_1_attributes.ino rename tasmota/tasmota_xdrv_driver/{xdrv_23_zigbee_5_converters.ino => xdrv_23_zigbee_5_2_converters.ino} (58%) create mode 100644 tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_7_6_plugin.ino create mode 100644 tasmota/zigbee/giex_water.zb diff --git a/CHANGELOG.md b/CHANGELOG.md index 02547ddaa..d56b21c5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. ## [12.1.0.1] ### Added +- Zigbee device plugin mechanism with commands ``ZbLoad``, ``ZbUnload`` and ``ZbLoadDump`` ### Changed diff --git a/tasmota/include/i18n.h b/tasmota/include/i18n.h index 54711b451..560baa0dc 100644 --- a/tasmota/include/i18n.h +++ b/tasmota/include/i18n.h @@ -672,6 +672,9 @@ #define D_JSON_ZIGBEE_SCAN "ZbScan" #define D_CMND_ZIGBEE_CIE "CIE" #define D_CMND_ZIGBEE_ENROLL "Enroll" +#define D_CMND_ZIGBEE_LOAD "Load" +#define D_CMND_ZIGBEE_LOADDUMP "LoadDump" +#define D_CMND_ZIGBEE_UNLOAD "Unload" // Commands xdrv_25_A4988_Stepper.ino #define D_CMND_MOTOR "MOTOR" diff --git a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_5__constants.ino b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_5_0_constants.ino similarity index 82% rename from tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_5__constants.ino rename to tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_5_0_constants.ino index df2d3c0be..cb395097f 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_5__constants.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_5_0_constants.ino @@ -356,9 +356,6 @@ const char Z_strings[] PROGMEM = "PhysicalClosedLimitLift" "\x00" "PhysicalClosedLimitTilt" "\x00" "Power" "\x00" - "Power2" "\x00" - "Power3" "\x00" - "Power4" "\x00" "PowerOffEffect" "\x00" "PowerOnRecall" "\x00" "PowerOnTimer" "\x00" @@ -440,32 +437,12 @@ const char Z_strings[] PROGMEM = "TimeStatus" "\x00" "TimeZone" "\x00" "TotalProfileNum" "\x00" - "TuyaAutoLock" "\x00" - "TuyaAwayDays" "\x00" - "TuyaAwayTemp" "\x00" - "TuyaBattery" "\x00" - "TuyaBoostTime" "\x00" "TuyaCalibration" "\x00" "TuyaCalibrationTime" "\x00" - "TuyaChildLock" "\x00" - "TuyaComfortTemp" "\x00" - "TuyaEcoTemp" "\x00" - "TuyaFanMode" "\x00" - "TuyaForceMode" "\x00" "TuyaMCUVersion" "\x00" - "TuyaMaxTemp" "\x00" - "TuyaMinTemp" "\x00" "TuyaMotorReversal" "\x00" "TuyaMovingState" "\x00" - "TuyaPreset" "\x00" "TuyaQuery" "\x00" - "TuyaScheduleHolidays" "\x00" - "TuyaScheduleWorkdays" "\x00" - "TuyaTempTarget" "\x00" - "TuyaValveDetection" "\x00" - "TuyaValvePosition" "\x00" - "TuyaWeekSelect" "\x00" - "TuyaWindowDetection" "\x00" "UnoccupiedCoolingSetpoint" "\x00" "UnoccupiedHeatingSetpoint" "\x00" "UtilityName" "\x00" @@ -792,158 +769,135 @@ enum Z_offsets { Zo_PhysicalClosedLimitLift = 4561, Zo_PhysicalClosedLimitTilt = 4585, Zo_Power = 4609, - Zo_Power2 = 4615, - Zo_Power3 = 4622, - Zo_Power4 = 4629, - Zo_PowerOffEffect = 4636, - Zo_PowerOnRecall = 4651, - Zo_PowerOnTimer = 4665, - Zo_PowerSource = 4678, - Zo_PowerThreshold = 4690, - Zo_Pressure = 4705, - Zo_PressureMaxMeasuredValue = 4714, - Zo_PressureMaxScaledValue = 4739, - Zo_PressureMinMeasuredValue = 4762, - Zo_PressureMinScaledValue = 4787, - Zo_PressureScale = 4810, - Zo_PressureScaledTolerance = 4824, - Zo_PressureScaledValue = 4848, - Zo_PressureTolerance = 4868, - Zo_Primary1Intensity = 4886, - Zo_Primary1X = 4904, - Zo_Primary1Y = 4914, - Zo_Primary2Intensity = 4924, - Zo_Primary2X = 4942, - Zo_Primary2Y = 4952, - Zo_Primary3Intensity = 4962, - Zo_Primary3X = 4980, - Zo_Primary3Y = 4990, - Zo_ProductCode = 5000, - Zo_ProductRevision = 5012, - Zo_ProductURL = 5028, - Zo_QualityMeasure = 5039, - Zo_RGB = 5054, - Zo_RMSCurrent = 5058, - Zo_RMSVoltage = 5069, - Zo_ReactivePower = 5080, - Zo_RecallScene = 5094, - Zo_RemainingTime = 5106, - Zo_RemoteSensing = 5120, - Zo_RemoveAllGroups = 5134, - Zo_RemoveAllScenes = 5150, - Zo_RemoveGroup = 5166, - Zo_RemoveScene = 5178, - Zo_ResetAlarm = 5190, - Zo_ResetAllAlarms = 5201, - Zo_SWBuildID = 5216, - Zo_Sat = 5226, - Zo_SatMove = 5230, - Zo_SatStep = 5238, - Zo_SceneCount = 5246, - Zo_SceneValid = 5257, - Zo_ScheduleMode = 5268, - Zo_SeaPressure = 5281, - Zo_ShortPollInterval = 5293, - Zo_Shutter = 5311, - Zo_ShutterClose = 5319, - Zo_ShutterLift = 5332, - Zo_ShutterOpen = 5344, - Zo_ShutterStop = 5356, - Zo_ShutterTilt = 5368, - Zo_SoftwareRevision = 5380, - Zo_StackVersion = 5397, - Zo_StandardTime = 5410, - Zo_StartUpOnOff = 5423, - Zo_Status = 5436, - Zo_StoreScene = 5443, - Zo_SwitchType = 5454, - Zo_SystemMode = 5465, - Zo_TRVBoost = 5476, - Zo_TRVChildProtection = 5485, - Zo_TRVMirrorDisplay = 5504, - Zo_TRVMode = 5521, - Zo_TRVWindowOpen = 5529, - Zo_TempTarget = 5543, - Zo_Temperature = 5554, - Zo_TemperatureMaxMeasuredValue = 5566, - Zo_TemperatureMinMeasuredValue = 5594, - Zo_TemperatureTolerance = 5622, - Zo_TerncyDuration = 5643, - Zo_TerncyRotate = 5658, - Zo_ThSetpoint = 5671, - Zo_Time = 5682, - Zo_TimeEpoch = 5687, - Zo_TimeStatus = 5697, - Zo_TimeZone = 5708, - Zo_TotalProfileNum = 5717, - Zo_TuyaAutoLock = 5733, - Zo_TuyaAwayDays = 5746, - Zo_TuyaAwayTemp = 5759, - Zo_TuyaBattery = 5772, - Zo_TuyaBoostTime = 5784, - Zo_TuyaCalibration = 5798, - Zo_TuyaCalibrationTime = 5814, - Zo_TuyaChildLock = 5834, - Zo_TuyaComfortTemp = 5848, - Zo_TuyaEcoTemp = 5864, - Zo_TuyaFanMode = 5876, - Zo_TuyaForceMode = 5888, - Zo_TuyaMCUVersion = 5902, - Zo_TuyaMaxTemp = 5917, - Zo_TuyaMinTemp = 5929, - Zo_TuyaMotorReversal = 5941, - Zo_TuyaMovingState = 5959, - Zo_TuyaPreset = 5975, - Zo_TuyaQuery = 5986, - Zo_TuyaScheduleHolidays = 5996, - Zo_TuyaScheduleWorkdays = 6017, - Zo_TuyaTempTarget = 6038, - Zo_TuyaValveDetection = 6053, - Zo_TuyaValvePosition = 6072, - Zo_TuyaWeekSelect = 6090, - Zo_TuyaWindowDetection = 6105, - Zo_UnoccupiedCoolingSetpoint = 6125, - Zo_UnoccupiedHeatingSetpoint = 6151, - Zo_UtilityName = 6177, - Zo_ValidUntilTime = 6189, - Zo_ValvePosition = 6204, - Zo_VelocityLift = 6218, - Zo_ViewGroup = 6231, - Zo_ViewScene = 6241, - Zo_Water = 6251, - Zo_WhitePointX = 6257, - Zo_WhitePointY = 6269, - Zo_WindowCoveringType = 6281, - Zo_X = 6300, - Zo_Y = 6302, - Zo_ZCLVersion = 6304, - Zo_ZoneState = 6315, - Zo_ZoneStatus = 6325, - Zo_ZoneStatusChange = 6336, - Zo_ZoneType = 6353, - Zo__ = 6362, - Zo_xx = 6364, - Zo_xx000A00 = 6367, - Zo_xx0A = 6376, - Zo_xx0A00 = 6381, - Zo_xx19 = 6388, - Zo_xx190A = 6393, - Zo_xx190A00 = 6400, - Zo_xxxx = 6409, - Zo_xxxx00 = 6414, - Zo_xxxx0A00 = 6421, - Zo_xxxxyy = 6430, - Zo_xxxxyyyy = 6437, - Zo_xxxxyyyy0A00 = 6446, - Zo_xxxxyyzz = 6459, - Zo_xxyy = 6468, - Zo_xxyy0A00 = 6473, - Zo_xxyyyy = 6482, - Zo_xxyyyy000000000000 = 6489, - Zo_xxyyyy0A0000000000 = 6508, - Zo_xxyyyyzz = 6527, - Zo_xxyyyyzzzz = 6536, - Zo_xxyyzzzz = 6547, + Zo_PowerOffEffect = 4615, + Zo_PowerOnRecall = 4630, + Zo_PowerOnTimer = 4644, + Zo_PowerSource = 4657, + Zo_PowerThreshold = 4669, + Zo_Pressure = 4684, + Zo_PressureMaxMeasuredValue = 4693, + Zo_PressureMaxScaledValue = 4718, + Zo_PressureMinMeasuredValue = 4741, + Zo_PressureMinScaledValue = 4766, + Zo_PressureScale = 4789, + Zo_PressureScaledTolerance = 4803, + Zo_PressureScaledValue = 4827, + Zo_PressureTolerance = 4847, + Zo_Primary1Intensity = 4865, + Zo_Primary1X = 4883, + Zo_Primary1Y = 4893, + Zo_Primary2Intensity = 4903, + Zo_Primary2X = 4921, + Zo_Primary2Y = 4931, + Zo_Primary3Intensity = 4941, + Zo_Primary3X = 4959, + Zo_Primary3Y = 4969, + Zo_ProductCode = 4979, + Zo_ProductRevision = 4991, + Zo_ProductURL = 5007, + Zo_QualityMeasure = 5018, + Zo_RGB = 5033, + Zo_RMSCurrent = 5037, + Zo_RMSVoltage = 5048, + Zo_ReactivePower = 5059, + Zo_RecallScene = 5073, + Zo_RemainingTime = 5085, + Zo_RemoteSensing = 5099, + Zo_RemoveAllGroups = 5113, + Zo_RemoveAllScenes = 5129, + Zo_RemoveGroup = 5145, + Zo_RemoveScene = 5157, + Zo_ResetAlarm = 5169, + Zo_ResetAllAlarms = 5180, + Zo_SWBuildID = 5195, + Zo_Sat = 5205, + Zo_SatMove = 5209, + Zo_SatStep = 5217, + Zo_SceneCount = 5225, + Zo_SceneValid = 5236, + Zo_ScheduleMode = 5247, + Zo_SeaPressure = 5260, + Zo_ShortPollInterval = 5272, + Zo_Shutter = 5290, + Zo_ShutterClose = 5298, + Zo_ShutterLift = 5311, + Zo_ShutterOpen = 5323, + Zo_ShutterStop = 5335, + Zo_ShutterTilt = 5347, + Zo_SoftwareRevision = 5359, + Zo_StackVersion = 5376, + Zo_StandardTime = 5389, + Zo_StartUpOnOff = 5402, + Zo_Status = 5415, + Zo_StoreScene = 5422, + Zo_SwitchType = 5433, + Zo_SystemMode = 5444, + Zo_TRVBoost = 5455, + Zo_TRVChildProtection = 5464, + Zo_TRVMirrorDisplay = 5483, + Zo_TRVMode = 5500, + Zo_TRVWindowOpen = 5508, + Zo_TempTarget = 5522, + Zo_Temperature = 5533, + Zo_TemperatureMaxMeasuredValue = 5545, + Zo_TemperatureMinMeasuredValue = 5573, + Zo_TemperatureTolerance = 5601, + Zo_TerncyDuration = 5622, + Zo_TerncyRotate = 5637, + Zo_ThSetpoint = 5650, + Zo_Time = 5661, + Zo_TimeEpoch = 5666, + Zo_TimeStatus = 5676, + Zo_TimeZone = 5687, + Zo_TotalProfileNum = 5696, + Zo_TuyaCalibration = 5712, + Zo_TuyaCalibrationTime = 5728, + Zo_TuyaMCUVersion = 5748, + Zo_TuyaMotorReversal = 5763, + Zo_TuyaMovingState = 5781, + Zo_TuyaQuery = 5797, + Zo_UnoccupiedCoolingSetpoint = 5807, + Zo_UnoccupiedHeatingSetpoint = 5833, + Zo_UtilityName = 5859, + Zo_ValidUntilTime = 5871, + Zo_ValvePosition = 5886, + Zo_VelocityLift = 5900, + Zo_ViewGroup = 5913, + Zo_ViewScene = 5923, + Zo_Water = 5933, + Zo_WhitePointX = 5939, + Zo_WhitePointY = 5951, + Zo_WindowCoveringType = 5963, + Zo_X = 5982, + Zo_Y = 5984, + Zo_ZCLVersion = 5986, + Zo_ZoneState = 5997, + Zo_ZoneStatus = 6007, + Zo_ZoneStatusChange = 6018, + Zo_ZoneType = 6035, + Zo__ = 6044, + Zo_xx = 6046, + Zo_xx000A00 = 6049, + Zo_xx0A = 6058, + Zo_xx0A00 = 6063, + Zo_xx19 = 6070, + Zo_xx190A = 6075, + Zo_xx190A00 = 6082, + Zo_xxxx = 6091, + Zo_xxxx00 = 6096, + Zo_xxxx0A00 = 6103, + Zo_xxxxyy = 6112, + Zo_xxxxyyyy = 6119, + Zo_xxxxyyyy0A00 = 6128, + Zo_xxxxyyzz = 6141, + Zo_xxyy = 6150, + Zo_xxyy0A00 = 6155, + Zo_xxyyyy = 6164, + Zo_xxyyyy000000000000 = 6171, + Zo_xxyyyy0A0000000000 = 6190, + Zo_xxyyyyzz = 6209, + Zo_xxyyyyzzzz = 6218, + Zo_xxyyzzzz = 6229, }; diff --git a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_5_1_attributes.ino b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_5_1_attributes.ino new file mode 100644 index 000000000..fcb0686da --- /dev/null +++ b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_5_1_attributes.ino @@ -0,0 +1,955 @@ +/* + xdrv_23_zigbee_converters.ino - zigbee support for Tasmota + + Copyright (C) 2021 Theo Arends and Stephan Hadinger + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifdef USE_ZIGBEE + +/*********************************************************************************************\ + * ZCL +\*********************************************************************************************/ + + +enum Z_DataTypes { + Znodata = 0x00, + Zdata8 = 0x08, Zdata16, Zdata24, Zdata32, Zdata40, Zdata48, Zdata56, Zdata64, + Zbool = 0x10, + Zmap8 = 0x18, Zmap16, Zmap24, Zmap32, Zmap40, Zmap48, Zmap56, Zmap64, + Zuint8 = 0x20, Zuint16, Zuint24, Zuint32, Zuint40, Zuint48, Zuint56, Zuint64, + Zint8 = 0x28, Zint16, Zint24, Zint32, Zint40, Zint48, Zint56, Zint64, + Zenum8 = 0x30, Zenum16 = 0x31, + Zsemi = 0x38, Zsingle = 0x39, Zdouble = 0x3A, + Zoctstr = 0x41, Zstring = 0x42, Zoctstr16 = 0x43, Zstring16 = 0x44, + Zarrray = 0x48, + Zstruct = 0x4C, + Zset = 0x50, Zbag = 0x51, + ZToD = 0xE0, Zdate = 0xE1, ZUTC = 0xE2, + ZclusterId = 0xE8, ZattribId = 0xE9, ZbacOID = 0xEA, + ZEUI64 = 0xF0, Zkey128 = 0xF1, + Zunk = 0xFF, + // adding fake type for Tuya specific encodings + Ztuya0 = Zoctstr, + Ztuya1 = Zbool, + Ztuya2 = Zint32, + Ztuya3 = Zstring, + Ztuya4 = Zuint8, + Ztuya5 = Zuint32 +}; + +const char Z_DATATYPES[] PROGMEM = + "nodata|" + "data8|data16|data24|data32|data40|data48|data56|data64|" + "bool|" + "map8|map16|map24|map32|map40|map48|map56|map64|" + "uint8|uint16|uint24|uint32|uint40|uint48|uint56|uint64|" + "int8|int16|int24|int32|int40|int48|int56|int64|" + "enum8|enum16|" + "semi|single|double|" + "octstr|string|octstr16|string16|" + "array|struct|set|bag|" + "ToD|date|UTC|" + "clusterId|attribId|bacOID|" + "EUI64|key128|" + "unk" +; + +const uint8_t Z_DATA_TYPES_CODE[] PROGMEM = { + Znodata, + Zdata8, Zdata16, Zdata24, Zdata32, Zdata40, Zdata48, Zdata56, Zdata64, + Zbool, + Zmap8, Zmap16, Zmap24, Zmap32, Zmap40, Zmap48, Zmap56, Zmap64, + Zuint8, Zuint16, Zuint24, Zuint32, Zuint40, Zuint48, Zuint56, Zuint64, + Zint8, Zint16, Zint24, Zint32, Zint40, Zint48, Zint56, Zint64, + Zenum8, Zenum16, + Zsemi, Zsingle, Zdouble, + Zoctstr, Zstring, Zoctstr16, Zstring16, + Zarrray, Zstruct, Zset, Zbag, + ZToD, Zdate, ZUTC, + ZclusterId, ZattribId, ZbacOID, + ZEUI64, Zkey128, + Zunk, +}; + +// convert a type into a name, or HEX if none found +void Z_getTypeByNumber(char *destination, size_t destination_size, uint8_t type) { + for (uint32_t i = 0; i < ARRAY_SIZE(Z_DATA_TYPES_CODE); i++) { + if (type == pgm_read_byte(&Z_DATA_TYPES_CODE[i])) { + GetTextIndexed(destination, destination_size, i, Z_DATATYPES); + return; + } + } + snprintf(destination, destination_size, "%02X", type); +} + +// convert a string to a type, or Zunk if no match +uint8_t Z_getTypeByName(const char *type) { + if (type == nullptr) { return Zunk; } + char type_found[16]; + int32_t ret = GetCommandCode(type_found, sizeof(type_found), type, Z_DATATYPES); + if (ret < 0) { + // try to decode hex + size_t type_len = strlen_P(type); + if (type_len > 0 && type_len <= 2) { + char *type_end; + ret = strtoul(type, &type_end, 16); + if (type_end == type) { ret = Zunk; } // could not decode + } + return ret; + } else { + return pgm_read_byte(&Z_DATA_TYPES_CODE[ret]); + } +} +// +// get the lenth in bytes for a data-type +// return 0 if unknown of type specific +// +// Note: this code is smaller than a static array +uint8_t Z_getDatatypeLen(uint8_t t) { + if ( ((t >= 0x08) && (t <= 0x0F)) || // data8 - data64 + ((t >= 0x18) && (t <= 0x2F)) ) { // map/uint/int + return (t & 0x07) + 1; + } + switch (t) { + case Zbool: + case Zenum8: + return 1; + case Zenum16: + case Zsemi: + case ZclusterId: + case ZattribId: + return 2; + case Zsingle: + case ZToD: + case Zdate: + case ZUTC: + case ZbacOID: + return 4; + case Zdouble: + case ZEUI64: + return 8; + case Zkey128: + return 16; + case Znodata: + default: + return 0; + } +} + +// is the type a discrete type, cf. section 2.6.2 of ZCL spec +bool Z_isDiscreteDataType(uint8_t t) { + if ( ((t >= 0x20) && (t <= 0x2F)) || // uint8 - int64 + ((t >= 0x38) && (t <= 0x3A)) || // semi - double + ((t >= 0xE0) && (t <= 0xE2)) ) { // ToD - UTC + return false; + } else { + return true; + } +} + +typedef struct Z_AttributeConverter { + uint8_t type; + uint8_t cluster_short; + uint16_t attribute; + uint16_t name_offset; + uint8_t multiplier_idx; // multiplier index for numerical value, use CmToMultiplier(), (if > 0 multiply by x, if <0 device by x) + // the high 4 bits are used to encode flags + // currently: 0x80 = this parameter needs to be exported to ZbData + uint8_t mapping; // high 4 bits = type, low 4 bits = offset in bytes from header + // still room for a byte +} Z_AttributeConverter; + +// Get offset in bytes of attributes, starting after the header (skipping first 4 bytes) +#define Z_OFFSET(c,a) (offsetof(class c, a) - sizeof(Z_Data)) +#define Z_CLASS(c) c // necessary to get a valid token without concatenation (which wouldn't work) +#define Z_MAPPING(c,a) (((((uint8_t)Z_CLASS(c)::type) & 0x0F) << 4) | Z_OFFSET(c,a)) + +// lines with this marker, will be used to export automatically data to `ZbData` +// at the condition Z_MAPPING() is also used +const uint8_t Z_EXPORT_DATA = 0x80; + +// Cluster numbers are store in 8 bits format to save space, +// the following tables allows the conversion from 8 bits index Cx... +// to the 16 bits actual cluster number +enum Cx_cluster_short { + Cx0000, Cx0001, Cx0002, Cx0003, Cx0004, Cx0005, Cx0006, Cx0007, + Cx0008, Cx0009, Cx000A, Cx000B, Cx000C, Cx000D, Cx000E, Cx000F, + Cx0010, Cx0011, Cx0012, Cx0013, Cx0014, Cx001A, Cx0020, Cx0100, + Cx0101, Cx0102, Cx0201, Cx0300, Cx0400, Cx0401, Cx0402, Cx0403, + Cx0404, Cx0405, Cx0406, Cx0500, Cx0702, Cx0B01, Cx0B04, Cx0B05, + CxEF00, CxFC01, CxFC40, CxFCC0, CxFCCC, +}; + +const uint16_t Cx_cluster[] PROGMEM = { + 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, + 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, + 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x001A, 0x0020, 0x0100, + 0x0101, 0x0102, 0x0201, 0x0300, 0x0400, 0x0401, 0x0402, 0x0403, + 0x0404, 0x0405, 0x0406, 0x0500, 0x0702, 0x0B01, 0x0B04, 0x0B05, + 0xEF00, 0xFC01, 0xFC40, 0xFCC0, 0xFCCC, +}; + +uint16_t CxToCluster(uint8_t cx) { + if (cx < nitems(Cx_cluster)) { + return pgm_read_word(&Cx_cluster[cx]); + } + return 0xFFFF; +} + +uint8_t ClusterToCx(uint16_t cluster) { + for (uint32_t i=0; icluster = cluster; + this->attribute = attribute; + this->name = name; + this->type = type; + } + + uint8_t type; // zigbee type, Zunk by default + int8_t multiplier; // multiplier, values 0, 1, 2, 5, 10, 100, -2, -5, -10, -100, + uint16_t cluster; // cluster number + uint16_t attribute; // attribute number + String name; // name of attribute once converted +}; + +// +// Class for an attribute synonym +// +class Z_attribute_synonym { +public: + Z_attribute_synonym(void) : + cluster(0xFFFF), attribute(0xFFFF), new_cluster(0xFFFF), new_attribute(0xFFFF), + multiplier(1) + {}; + + void set(uint16_t cluster, uint16_t attribute, uint16_t new_cluster, uint16_t new_attribute, int8_t multiplier = 1) { + this->cluster = cluster; + this->attribute = attribute; + this->new_cluster = new_cluster; + this->new_attribute = new_attribute; + this->multiplier = multiplier; + } + + inline bool found(void) const { return cluster != 0xFFFF && attribute != 0xFFFF; } + + uint16_t cluster; // cluster to match + uint16_t attribute; // attribute to match + uint16_t new_cluster; // replace with this cluster + uint16_t new_attribute; // replace with this attribute + int8_t multiplier; // mutliplier if different than 1 (otherwise don't change value) +}; + +// +// +// matcher for a device, based on ModelID and Manufacturer +// +// +class Z_plugin_matcher { +public: + + Z_plugin_matcher(void) {}; + + inline void setModelManuf(const char *_model, const char *_manuf) { + model = (const char*)_model; + manufacturer = (const char*)_manuf; + } + + bool match(const char *match_model, const char *match_manuf) const { + bool match = true; + if (model.length() > 0) { + if (!model.equals(match_model)) { + match = false; + } + } + if (manufacturer.length() > 0) { + if (!manufacturer.equals(match_manuf)) { + match = false; + } + } + return match; + } + + String model; + String manufacturer; +}; + +// +// +// Z_plugin_template : template for a group of devices +// +// +class Z_plugin_template { +public: + + LList matchers; + LList synonyms; + LList attributes; + char * filename = nullptr; // the filename from which it was imported + // needs to be freed by ZbUnload only (to allow reuse between mutliple instances) +}; + +// +// +// Z_plugin_templates +// +// +class Z_plugin_templates : public LList { +public: + // match an attribute from the plug-in in-memory database + // returns `nullptr` if none found + // or a pointer to `Z_plugin_attribute` + const Z_plugin_attribute * matchAttributeById(const char *model, const char *manufacturer, uint16_t cluster, uint16_t attribute); + const Z_plugin_attribute * matchAttributeByName(const char *model, const char *manufacturer, const char *name); + const Z_attribute_synonym * matchAttributeSynonym(const char *model, const char *manufacturer, uint16_t cluster, uint16_t attribute); +}; + +const Z_attribute_synonym * Z_plugin_templates::matchAttributeSynonym(const char *model, const char *manufacturer, uint16_t cluster, uint16_t attribute) { + // scan through all templates + for (const Z_plugin_template & tmpl : *this) { + const LList & matchers = tmpl.matchers; // get synonyms + const LList & synonyms = tmpl.synonyms; // get synonyms + + for (const Z_plugin_matcher & mtch : matchers) { + if (mtch.match(model, manufacturer)) { + // got a match, apply template + for (const Z_attribute_synonym & syn : synonyms) { + if (syn.cluster == cluster && syn.attribute == attribute) { + return &syn; + } + } + } + } + } + // no match + return nullptr; +} + +const Z_plugin_attribute * Z_plugin_templates::matchAttributeById(const char *model, const char *manufacturer, uint16_t cluster, uint16_t attribute) { + // scan through all templates + for (const Z_plugin_template & tmpl : *this) { + const LList & matchers = tmpl.matchers; // get synonyms + const LList & attributes = tmpl.attributes; // get synonyms + + for (const Z_plugin_matcher & mtch : matchers) { + if (mtch.match(model, manufacturer)) { + // got a match, apply template + for (const Z_plugin_attribute & attr : attributes) { + if (attr.cluster == cluster && attr.attribute == attribute) { + return &attr; + } + } + } + } + } + // no match + return nullptr; +} + +const Z_plugin_attribute * Z_plugin_templates::matchAttributeByName(const char *model, const char *manufacturer, const char * name) { + // scan through all templates + for (const Z_plugin_template & tmpl : *this) { + const LList & matchers = tmpl.matchers; // get synonyms + const LList & attributes = tmpl.attributes; // get synonyms + + for (const Z_plugin_matcher & mtch : matchers) { + if (mtch.match(model, manufacturer)) { + // got a match, apply template + for (const Z_plugin_attribute & attr : attributes) { + if (attr.name.equals(name)) { + return &attr; + } + } + } + } + } + // no match + return nullptr; +} + +// +// Attribute match result +// +class Z_attribute_match { +public: + inline bool found(void) const { return (cluster != 0xFFFF && attribute != 0xFFFF); } + + uint16_t cluster = 0xFFFF; + uint16_t attribute = 0xFFFF; + const char * name = nullptr; + uint8_t zigbee_type = Znodata; + int8_t multiplier = 1; + uint8_t map_offset = 0; + Z_Data_Type map_type = Z_Data_Type::Z_Unknown; +}; + +Z_attribute_match Z_plugin_matchAttributeById(const char *model, const char *manufacturer, uint16_t cluster, uint16_t attribute); +Z_attribute_match Z_plugin_matchAttributeByName(const char *model, const char *manufacturer, const char *name); + + +// list of post-processing directives +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Winvalid-offsetof" // avoid warnings since we're using offsetof() in a risky way +const Z_AttributeConverter Z_PostProcess[] PROGMEM = { + { Zuint8, Cx0000, 0x0000, Z_(ZCLVersion), Cm1, 0 }, + { Zuint8, Cx0000, 0x0001, Z_(AppVersion), Cm1, 0 }, + { Zuint8, Cx0000, 0x0002, Z_(StackVersion), Cm1, 0 }, + { Zuint8, Cx0000, 0x0003, Z_(HWVersion), Cm1, 0 }, + { Zstring, Cx0000, 0x0004, Z_(Manufacturer), Cm1, 0 }, // record Manufacturer + { Zstring, Cx0000, 0x0005, Z_(ModelId), Cm1, 0 }, // record Model + // { Zstring, Cx0000, 0x0004, Z_(Manufacturer), Cm1, Z_ManufKeep, 0 }, // record Manufacturer + // { Zstring, Cx0000, 0x0005, Z_(ModelId), Cm1, Z_ModelKeep, 0 }, // record Model + { Zstring, Cx0000, 0x0006, Z_(DateCode), Cm1, 0 }, + { Zenum8, Cx0000, 0x0007, Z_(PowerSource), Cm1, 0 }, + { Zenum8, Cx0000, 0x0008, Z_(GenericDeviceClass), Cm1, 0 }, + { Zenum8, Cx0000, 0x0009, Z_(GenericDeviceType), Cm1, 0 }, + { Zoctstr, Cx0000, 0x000A, Z_(ProductCode), Cm1, 0 }, + { Zstring, Cx0000, 0x000B, Z_(ProductURL), Cm1, 0 }, + { Zstring, Cx0000, 0x4000, Z_(SWBuildID), Cm1, 0 }, + { Zuint8, Cx0000, 0x4005, Z_(MullerLightMode), Cm1, 0 }, + // { Zunk, Cx0000, 0xFFFF, nullptr, Cm0, 0 }, // Remove all other values + // Cmd 0x0A - Cluster 0x0000, attribute 0xFF01 - proprietary + { Zmap8, Cx0000, 0xFF01, Z_(), Cm0, 0 }, + { Zmap8, Cx0000, 0xFF02, Z_(), Cm0, 0 }, + // { Zmap8, Cx0000, 0xFF01, Z_(), Cm0, Z_AqaraSensor, 0 }, + // { Zmap8, Cx0000, 0xFF02, Z_(), Cm0, Z_AqaraSensor2, 0 }, + + // Power Configuration cluster + { Zuint16, Cx0001, 0x0000, Z_(MainsVoltage), Cm1, 0 }, + { Zuint8, Cx0001, 0x0001, Z_(MainsFrequency), Cm1, 0 }, + { Zuint8, Cx0001, 0x0020, Z_(BatteryVoltage), Cm_10, 0 }, // divide by 10 + { Zuint8, Cx0001, 0x0021, Z_(BatteryPercentage), Cm_2, 0 }, // divide by 2 + // { Zuint8, Cx0001, 0x0021, Z_(BatteryPercentage), Cm_2, Z_BatteryPercentage, 0 }, // divide by 2 + + // Device Temperature Configuration cluster + { Zint16, Cx0002, 0x0000, Z_(CurrentTemperature), Cm1, 0 }, + { Zint16, Cx0002, 0x0001, Z_(MinTempExperienced), Cm1, 0 }, + { Zint16, Cx0002, 0x0002, Z_(MaxTempExperienced), Cm1, 0 }, + { Zuint16, Cx0002, 0x0003, Z_(OverTempTotalDwell), Cm1, 0 }, + + // Identify cluster + { Zuint16, Cx0003, 0x0000, Z_(IdentifyTime), Cm1, 0 }, + + // Groups cluster + { Zmap8, Cx0004, 0x0000, Z_(GroupNameSupport), Cm1, 0 }, + + // Scenes cluster + { Zuint8, Cx0005, 0x0000, Z_(SceneCount), Cm1, 0 }, + { Zuint8, Cx0005, 0x0001, Z_(CurrentScene), Cm1, 0 }, + { Zuint16, Cx0005, 0x0002, Z_(CurrentGroup), Cm1, 0 }, + { Zbool, Cx0005, 0x0003, Z_(SceneValid), Cm1, 0 }, + //{ Zmap8, Cx0005, 0x0004, (NameSupport), Cm1, 0 }, + + // On/off cluster + { Zbool, Cx0006, 0x0000, Z_(Power), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_OnOff, power) }, + { Zenum8, Cx0006, 0x4003, Z_(StartUpOnOff), Cm1, 0 }, + { Zbool, Cx0006, 0x8000, Z_(Power), Cm1, 0 }, // See 7280 + + // On/Off Switch Configuration cluster + { Zenum8, Cx0007, 0x0000, Z_(SwitchType), Cm1, 0 }, + + // Level Control cluster + { Zuint8, Cx0008, 0x0000, Z_(Dimmer), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_Light, dimmer) }, + { Zmap8, Cx0008, 0x000F, Z_(DimmerOptions), Cm1, 0 }, + { Zuint16, Cx0008, 0x0001, Z_(DimmerRemainingTime), Cm1, 0 }, + { Zuint16, Cx0008, 0x0010, Z_(OnOffTransitionTime), Cm1, 0 }, + // { Zuint8, Cx0008, 0x0011, (OnLevel), Cm1, 0 }, + // { Zuint16, Cx0008, 0x0012, (OnTransitionTime), Cm1, 0 }, + // { Zuint16, Cx0008, 0x0013, (OffTransitionTime), Cm1, 0 }, + // { Zuint16, Cx0008, 0x0014, (DefaultMoveRate), Cm1, 0 }, + + // Alarms cluster + { Zuint16, Cx0009, 0x0000, Z_(AlarmCount), Cm1, 0 }, + + // Time cluster + { ZUTC, Cx000A, 0x0000, Z_(Time), Cm1, 0 }, + { Zmap8, Cx000A, 0x0001, Z_(TimeStatus), Cm1, 0 }, + { Zint32, Cx000A, 0x0002, Z_(TimeZone), Cm1, 0 }, + { Zuint32, Cx000A, 0x0003, Z_(DstStart), Cm1, 0 }, + { Zuint32, Cx000A, 0x0004, Z_(DstEnd), Cm1, 0 }, + { Zint32, Cx000A, 0x0005, Z_(DstShift), Cm1, 0 }, + { Zuint32, Cx000A, 0x0006, Z_(StandardTime), Cm1, 0 }, + { Zuint32, Cx000A, 0x0007, Z_(LocalTime), Cm1, 0 }, + { ZUTC, Cx000A, 0x0008, Z_(LastSetTime), Cm1, 0 }, + { ZUTC, Cx000A, 0x0009, Z_(ValidUntilTime), Cm1, 0 }, + { ZUTC, Cx000A, 0xFF00, Z_(TimeEpoch), Cm1, 0 }, // Tasmota specific, epoch + + // RSSI Location cluster + { Zdata8, Cx000B, 0x0000, Z_(LocationType), Cm1, 0 }, + { Zenum8, Cx000B, 0x0001, Z_(LocationMethod), Cm1, 0 }, + { Zuint16, Cx000B, 0x0002, Z_(LocationAge), Cm1, 0 }, + { Zuint8, Cx000B, 0x0003, Z_(QualityMeasure), Cm1, 0 }, + { Zuint8, Cx000B, 0x0004, Z_(NumberOfDevices), Cm1, 0 }, + + // Analog Input cluster + // { 0xFF, Cx000C, 0x0004, (AnalogInActiveText), Cm1, 0 }, + { Zstring, Cx000C, 0x001C, Z_(AnalogInDescription), Cm1, 0 }, + // { 0xFF, Cx000C, 0x002E, (AnalogInInactiveText), Cm1, 0 }, + { Zsingle, Cx000C, 0x0041, Z_(AnalogInMaxValue), Cm1, 0 }, + { Zsingle, Cx000C, 0x0045, Z_(AnalogInMinValue), Cm1, 0 }, + { Zbool, Cx000C, 0x0051, Z_(AnalogInOutOfService), Cm1, 0 }, + { Zsingle, Cx000C, 0x0055, Z_(AnalogValue), Cm1, 0 }, + // { 0xFF, Cx000C, 0x0057, (AnalogInPriorityArray),Cm1, 0 }, + { Zenum8, Cx000C, 0x0067, Z_(AnalogInReliability), Cm1, 0 }, + // { 0xFF, Cx000C, 0x0068, (AnalogInRelinquishDefault),Cm1, 0 }, + { Zsingle, Cx000C, 0x006A, Z_(AnalogInResolution), Cm1, 0 }, + { Zmap8, Cx000C, 0x006F, Z_(AnalogInStatusFlags), Cm1, 0 }, + { Zenum16, Cx000C, 0x0075, Z_(AnalogInEngineeringUnits),Cm1, 0 }, + { Zuint32, Cx000C, 0x0100, Z_(AnalogInApplicationType),Cm1, 0 }, + { Zuint16, Cx000C, 0xFF55, Z_(AqaraRotate), Cm1, 0 }, + { Zuint16, Cx000C, 0xFF05, Z_(Aqara_FF05), Cm1, 0 }, + + // Analog Output cluster + { Zstring, Cx000D, 0x001C, Z_(AnalogOutDescription), Cm1, 0 }, + { Zsingle, Cx000D, 0x0041, Z_(AnalogOutMaxValue), Cm1, 0 }, + { Zsingle, Cx000D, 0x0045, Z_(AnalogOutMinValue), Cm1, 0 }, + { Zbool, Cx000D, 0x0051, Z_(AnalogOutOutOfService),Cm1, 0 }, + { Zsingle, Cx000D, 0x0055, Z_(AnalogOutValue), Cm1, 0 }, + // { Zunk, Cx000D, 0x0057, (AnalogOutPriorityArray),Cm1, 0 }, + { Zenum8, Cx000D, 0x0067, Z_(AnalogOutReliability), Cm1, 0 }, + { Zsingle, Cx000D, 0x0068, Z_(AnalogOutRelinquishDefault), Cm1, 0 }, + { Zsingle, Cx000D, 0x006A, Z_(AnalogOutResolution), Cm1, 0 }, + { Zmap8, Cx000D, 0x006F, Z_(AnalogOutStatusFlags), Cm1, 0 }, + { Zenum16, Cx000D, 0x0075, Z_(AnalogOutEngineeringUnits), Cm1, 0 }, + { Zuint32, Cx000D, 0x0100, Z_(AnalogOutApplicationType), Cm1, 0 }, + + // Analog Value cluster + { Zstring, Cx000E, 0x001C, Z_(AnalogDescription), Cm1, 0 }, + { Zbool, Cx000E, 0x0051, Z_(AnalogOutOfService), Cm1, 0 }, + { Zsingle, Cx000E, 0x0055, Z_(AnalogValue), Cm1, 0 }, + { Zunk, Cx000E, 0x0057, Z_(AnalogPriorityArray), Cm1, 0 }, + { Zenum8, Cx000E, 0x0067, Z_(AnalogReliability), Cm1, 0 }, + { Zsingle, Cx000E, 0x0068, Z_(AnalogRelinquishDefault),Cm1, 0 }, + { Zmap8, Cx000E, 0x006F, Z_(AnalogStatusFlags), Cm1, 0 }, + { Zenum16, Cx000E, 0x0075, Z_(AnalogEngineeringUnits),Cm1, 0 }, + { Zuint32, Cx000E, 0x0100, Z_(AnalogApplicationType),Cm1, 0 }, + + // Binary Input cluster + { Zstring, Cx000F, 0x0004, Z_(BinaryInActiveText), Cm1, 0 }, + { Zstring, Cx000F, 0x001C, Z_(BinaryInDescription), Cm1, 0 }, + { Zstring, Cx000F, 0x002E, Z_(BinaryInInactiveText),Cm1, 0 }, + { Zbool, Cx000F, 0x0051, Z_(BinaryInOutOfService),Cm1, 0 }, + { Zenum8, Cx000F, 0x0054, Z_(BinaryInPolarity), Cm1, 0 }, + { Zstring, Cx000F, 0x0055, Z_(BinaryInValue), Cm1, 0 }, + // { 0xFF, Cx000F, 0x0057, (BinaryInPriorityArray),Cm1, 0 }, + { Zenum8, Cx000F, 0x0067, Z_(BinaryInReliability), Cm1, 0 }, + { Zmap8, Cx000F, 0x006F, Z_(BinaryInStatusFlags), Cm1, 0 }, + { Zuint32, Cx000F, 0x0100, Z_(BinaryInApplicationType),Cm1, 0 }, + + // Binary Output cluster + { Zstring, Cx0010, 0x0004, Z_(BinaryOutActiveText), Cm1, 0 }, + { Zstring, Cx0010, 0x001C, Z_(BinaryOutDescription), Cm1, 0 }, + { Zstring, Cx0010, 0x002E, Z_(BinaryOutInactiveText),Cm1, 0 }, + { Zuint32, Cx0010, 0x0042, Z_(BinaryOutMinimumOffTime),Cm1, 0 }, + { Zuint32, Cx0010, 0x0043, Z_(BinaryOutMinimumOnTime),Cm1, 0 }, + { Zbool, Cx0010, 0x0051, Z_(BinaryOutOutOfService),Cm1, 0 }, + { Zenum8, Cx0010, 0x0054, Z_(BinaryOutPolarity), Cm1, 0 }, + { Zbool, Cx0010, 0x0055, Z_(BinaryOutValue), Cm1, 0 }, + // { Zunk, Cx0010, 0x0057, (BinaryOutPriorityArray),Cm1, 0 }, + { Zenum8, Cx0010, 0x0067, Z_(BinaryOutReliability), Cm1, 0 }, + { Zbool, Cx0010, 0x0068, Z_(BinaryOutRelinquishDefault),Cm1, 0 }, + { Zmap8, Cx0010, 0x006F, Z_(BinaryOutStatusFlags), Cm1, 0 }, + { Zuint32, Cx0010, 0x0100, Z_(BinaryOutApplicationType),Cm1, 0 }, + + // Binary Value cluster + { Zstring, Cx0011, 0x0004, Z_(BinaryActiveText), Cm1, 0 }, + { Zstring, Cx0011, 0x001C, Z_(BinaryDescription), Cm1, 0 }, + { Zstring, Cx0011, 0x002E, Z_(BinaryInactiveText), Cm1, 0 }, + { Zuint32, Cx0011, 0x0042, Z_(BinaryMinimumOffTime), Cm1, 0 }, + { Zuint32, Cx0011, 0x0043, Z_(BinaryMinimumOnTime), Cm1, 0 }, + { Zbool, Cx0011, 0x0051, Z_(BinaryOutOfService), Cm1, 0 }, + { Zbool, Cx0011, 0x0055, Z_(BinaryValue), Cm1, 0 }, + // { Zunk, Cx0011, 0x0057, (BinaryPriorityArray), Cm1, 0 }, + { Zenum8, Cx0011, 0x0067, Z_(BinaryReliability), Cm1, 0 }, + { Zbool, Cx0011, 0x0068, Z_(BinaryRelinquishDefault),Cm1, 0 }, + { Zmap8, Cx0011, 0x006F, Z_(BinaryStatusFlags), Cm1, 0 }, + { Zuint32, Cx0011, 0x0100, Z_(BinaryApplicationType),Cm1, 0 }, + + // Multistate Input cluster + // { Zunk, Cx0012, 0x000E, (MultiInStateText), Cm1, 0 }, + { Zstring, Cx0012, 0x001C, Z_(MultiInDescription), Cm1, 0 }, + { Zuint16, Cx0012, 0x004A, Z_(MultiInNumberOfStates),Cm1, 0 }, + { Zbool, Cx0012, 0x0051, Z_(MultiInOutOfService), Cm1, 0 }, + { Zuint16, Cx0012, 0x0055, Z_(MultiInValue), Cm1, 0 }, + // { Zuint16, Cx0012, 0x0055, Z_(MultiInValue), Cm0, Z_AqaraCube, 0 }, + // { Zuint16, Cx0012, 0x0055, Z_(MultiInValue), Cm0, Z_AqaraButton, 0 }, + { Zenum8, Cx0012, 0x0067, Z_(MultiInReliability), Cm1, 0 }, + { Zmap8, Cx0012, 0x006F, Z_(MultiInStatusFlags), Cm1, 0 }, + { Zuint32, Cx0012, 0x0100, Z_(MultiInApplicationType),Cm1, 0 }, + + // Multistate output + // { Zunk, Cx0013, 0x000E, (MultiOutStateText), Cm1, 0 }, + { Zstring, Cx0013, 0x001C, Z_(MultiOutDescription), Cm1, 0 }, + { Zuint16, Cx0013, 0x004A, Z_(MultiOutNumberOfStates),Cm1, 0 }, + { Zbool, Cx0013, 0x0051, Z_(MultiOutOutOfService), Cm1, 0 }, + { Zuint16, Cx0013, 0x0055, Z_(MultiOutValue), Cm1, 0 }, + // { Zunk, Cx0013, 0x0057, (MultiOutPriorityArray),Cm1, 0 }, + { Zenum8, Cx0013, 0x0067, Z_(MultiOutReliability), Cm1, 0 }, + { Zuint16, Cx0013, 0x0068, Z_(MultiOutRelinquishDefault),Cm1, 0 }, + { Zmap8, Cx0013, 0x006F, Z_(MultiOutStatusFlags), Cm1, 0 }, + { Zuint32, Cx0013, 0x0100, Z_(MultiOutApplicationType),Cm1, 0 }, + + // Multistate Value cluster + // { Zunk, Cx0014, 0x000E, (MultiStateText), Cm1, 0 }, + { Zstring, Cx0014, 0x001C, Z_(MultiDescription), Cm1, 0 }, + { Zuint16, Cx0014, 0x004A, Z_(MultiNumberOfStates), Cm1, 0 }, + { Zbool, Cx0014, 0x0051, Z_(MultiOutOfService), Cm1, 0 }, + { Zuint16, Cx0014, 0x0055, Z_(MultiValue), Cm1, 0 }, + { Zenum8, Cx0014, 0x0067, Z_(MultiReliability), Cm1, 0 }, + { Zuint16, Cx0014, 0x0068, Z_(MultiRelinquishDefault),Cm1, 0 }, + { Zmap8, Cx0014, 0x006F, Z_(MultiStatusFlags), Cm1, 0 }, + { Zuint32, Cx0014, 0x0100, Z_(MultiApplicationType), Cm1, 0 }, + + // Power Profile cluster + { Zuint8, Cx001A, 0x0000, Z_(TotalProfileNum), Cm1, 0 }, + { Zbool, Cx001A, 0x0001, Z_(MultipleScheduling), Cm1, 0 }, + { Zmap8, Cx001A, 0x0002, Z_(EnergyFormatting), Cm1, 0 }, + { Zbool, Cx001A, 0x0003, Z_(EnergyRemote), Cm1, 0 }, + { Zmap8, Cx001A, 0x0004, Z_(ScheduleMode), Cm1, 0 }, + + // Poll Control cluster + { Zuint32, Cx0020, 0x0000, Z_(CheckinInterval), Cm1, 0 }, + { Zuint32, Cx0020, 0x0001, Z_(LongPollInterval), Cm1, 0 }, + { Zuint16, Cx0020, 0x0002, Z_(ShortPollInterval), Cm1, 0 }, + { Zuint16, Cx0020, 0x0003, Z_(FastPollTimeout), Cm1, 0 }, + { Zuint32, Cx0020, 0x0004, Z_(CheckinIntervalMin), Cm1, 0 }, + { Zuint32, Cx0020, 0x0005, Z_(LongPollIntervalMin), Cm1, 0 }, + { Zuint16, Cx0020, 0x0006, Z_(FastPollTimeoutMax), Cm1, 0 }, + + // Shade Configuration cluster + { Zuint16, Cx0100, 0x0000, Z_(PhysicalClosedLimit), Cm1, 0 }, + { Zuint8, Cx0100, 0x0001, Z_(MotorStepSize), Cm1, 0 }, + { Zmap8, Cx0100, 0x0002, Z_(Status), Cm1, 0 }, + { Zuint16, Cx0100, 0x0010, Z_(ClosedLimit), Cm1, 0 }, + { Zenum8, Cx0100, 0x0011, Z_(Mode), Cm1, 0 }, + + // Door Lock cluster + { Zenum8, Cx0101, 0x0000, Z_(LockState), Cm1, 0 }, + { Zenum8, Cx0101, 0x0001, Z_(LockType), Cm1, 0 }, + { Zbool, Cx0101, 0x0002, Z_(ActuatorEnabled), Cm1, 0 }, + { Zenum8, Cx0101, 0x0003, Z_(DoorState), Cm1, 0 }, + { Zuint32, Cx0101, 0x0004, Z_(DoorOpenEvents), Cm1, 0 }, + { Zuint32, Cx0101, 0x0005, Z_(DoorClosedEvents), Cm1, 0 }, + { Zuint16, Cx0101, 0x0006, Z_(OpenPeriod), Cm1, 0 }, + + // Aqara Lumi Vibration Sensor + { Zuint16, Cx0101, 0x0055, Z_(AqaraVibrationMode), Cm1, 0 }, + { Zuint16, Cx0101, 0x0503, Z_(AqaraVibrationsOrAngle), Cm1, 0 }, + { Zuint32, Cx0101, 0x0505, Z_(AqaraVibration505), Cm1, 0 }, + { Zuint48, Cx0101, 0x0508, Z_(AqaraAccelerometer), Cm1, 0 }, + + // Window Covering cluster + { Zenum8, Cx0102, 0x0000, Z_(WindowCoveringType), Cm1, 0 }, + { Zuint16, Cx0102, 0x0001, Z_(PhysicalClosedLimitLift),Cm1, 0 }, + { Zuint16, Cx0102, 0x0002, Z_(PhysicalClosedLimitTilt),Cm1, 0 }, + { Zuint16, Cx0102, 0x0003, Z_(CurrentPositionLift), Cm1, 0 }, + { Zuint16, Cx0102, 0x0004, Z_(CurrentPositionTilt), Cm1, 0 }, + { Zuint16, Cx0102, 0x0005, Z_(NumberofActuationsLift),Cm1, 0 }, + { Zuint16, Cx0102, 0x0006, Z_(NumberofActuationsTilt),Cm1, 0 }, + { Zmap8, Cx0102, 0x0007, Z_(ConfigStatus), Cm1, 0 }, + { Zuint8, Cx0102, 0x0008, Z_(CurrentPositionLiftPercentage),Cm1, 0 }, + { Zuint8, Cx0102, 0x0009, Z_(CurrentPositionTiltPercentage),Cm1, 0 }, + { Zuint16, Cx0102, 0x0010, Z_(InstalledOpenLimitLift),Cm1, 0 }, + { Zuint16, Cx0102, 0x0011, Z_(InstalledClosedLimitLift),Cm1, 0 }, + { Zuint16, Cx0102, 0x0012, Z_(InstalledOpenLimitTilt),Cm1, 0 }, + { Zuint16, Cx0102, 0x0013, Z_(InstalledClosedLimitTilt),Cm1, 0 }, + { Zuint16, Cx0102, 0x0014, Z_(VelocityLift), Cm1, 0 }, + { Zuint16, Cx0102, 0x0015, Z_(AccelerationTimeLift),Cm1, 0 }, + { Zuint16, Cx0102, 0x0016, Z_(DecelerationTimeLift), Cm1, 0 }, + { Zmap8, Cx0102, 0x0017, Z_(Mode), Cm1, 0 }, + { Zoctstr, Cx0102, 0x0018, Z_(IntermediateSetpointsLift),Cm1, 0 }, + { Zoctstr, Cx0102, 0x0019, Z_(IntermediateSetpointsTilt),Cm1, 0 }, + // Tuya specific, from Zigbee2MQTT https://github.com/Koenkk/zigbee-herdsman/blob/4fed7310d1e1e9d81e5148cf5b4d8ec98d03c687/src/zcl/definition/cluster.ts#L1772 + { Zenum8, Cx0102, 0xF000, Z_(TuyaMovingState),Cm1, 0 }, + { Zenum8, Cx0102, 0xF001, Z_(TuyaCalibration),Cm1, 0 }, + { Zenum8, Cx0102, 0xF002, Z_(TuyaMotorReversal),Cm1, 0 }, + { Zuint16, Cx0102, 0xF003, Z_(TuyaCalibrationTime),Cm1, 0 }, + + // Thermostat + { Zint16, Cx0201, 0x0000, Z_(LocalTemperature), Cm_100, Z_MAPPING(Z_Data_Thermo, temperature) }, + { Zint16, Cx0201, 0x0001, Z_(OutdoorTemperature),Cm_100, 0 }, + { Zuint8, Cx0201, 0x0007, Z_(PICoolingDemand), Cm1, Z_MAPPING(Z_Data_Thermo, th_setpoint) }, + { Zuint8, Cx0201, 0x0008, Z_(PIHeatingDemand), Cm1, Z_MAPPING(Z_Data_Thermo, th_setpoint) }, + { Zint8, Cx0201, 0x0010, Z_(LocalTemperatureCalibration), Cm_10, 0 }, + { Zint16, Cx0201, 0x0011, Z_(OccupiedCoolingSetpoint), Cm_100, Z_MAPPING(Z_Data_Thermo, temperature_target) }, + { Zint16, Cx0201, 0x0012, Z_(OccupiedHeatingSetpoint), Cm_100, Z_MAPPING(Z_Data_Thermo, temperature_target) }, + { Zint16, Cx0201, 0x0013, Z_(UnoccupiedCoolingSetpoint), Cm_100, 0 }, + { Zint16, Cx0201, 0x0014, Z_(UnoccupiedHeatingSetpoint), Cm_100, 0 }, + { Zmap8, Cx0201, 0x001A, Z_(RemoteSensing), Cm1, 0 }, + { Zenum8, Cx0201, 0x001B, Z_(ControlSequenceOfOperation), Cm1, 0 }, + { Zenum8, Cx0201, 0x001C, Z_(SystemMode), Cm1, 0 }, + // below is Eurotronic specific + { Zenum8, Cx0201, 0x4000, Z_(TRVMode), Cm1, 0 }, + { Zuint8, Cx0201, 0x4001, Z_(ValvePosition), Cm1, 0 }, + { Zuint8, Cx0201, 0x4002, Z_(EurotronicErrors), Cm1, 0 }, + { Zint16, Cx0201, 0x4003, Z_(CurrentTemperatureSetPoint), Cm_100, 0 }, + { Zuint24, Cx0201, 0x4008, Z_(EurotronicHostFlags), Cm1, 0 }, + // below are synthetic virtual attributes used to decode EurotronicHostFlags + // Last byte acts as a field mask for the lowest byte value + { Zbool, Cx0201, 0xF002, Z_(TRVMirrorDisplay), Cm1, 0 }, + { Zbool, Cx0201, 0xF004, Z_(TRVBoost), Cm1, 0 }, + { Zbool, Cx0201, 0xF010, Z_(TRVWindowOpen), Cm1, 0 }, + { Zbool, Cx0201, 0xF080, Z_(TRVChildProtection), Cm1, 0 }, + // below are virtual attributes to simplify ZbData import/export + { Zuint8, Cx0201, 0xFFF0, Z_(ThSetpoint), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_Thermo, th_setpoint) }, + { Zint16, Cx0201, 0xFFF1, Z_(TempTarget), Cm_100 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_Thermo, temperature_target) }, + + // Color Control cluster + { Zuint8, Cx0300, 0x0000, Z_(Hue), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_Light, hue) }, + { Zuint8, Cx0300, 0x0001, Z_(Sat), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_Light, sat) }, + { Zuint16, Cx0300, 0x0002, Z_(RemainingTime), Cm1, 0 }, + { Zuint16, Cx0300, 0x0003, Z_(X), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_Light, x) }, + { Zuint16, Cx0300, 0x0004, Z_(Y), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_Light, y) }, + { Zenum8, Cx0300, 0x0005, Z_(DriftCompensation), Cm1, 0 }, + { Zstring, Cx0300, 0x0006, Z_(CompensationText), Cm1, 0 }, + { Zuint16, Cx0300, 0x0007, Z_(CT), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_Light, ct) }, + { Zenum8, Cx0300, 0x0008, Z_(ColorMode), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_Light, colormode) }, + { Zuint8, Cx0300, 0x0010, Z_(NumberOfPrimaries), Cm1, 0 }, + { Zuint16, Cx0300, 0x0011, Z_(Primary1X), Cm1, 0 }, + { Zuint16, Cx0300, 0x0012, Z_(Primary1Y), Cm1, 0 }, + { Zuint8, Cx0300, 0x0013, Z_(Primary1Intensity), Cm1, 0 }, + { Zuint16, Cx0300, 0x0015, Z_(Primary2X), Cm1, 0 }, + { Zuint16, Cx0300, 0x0016, Z_(Primary2Y), Cm1, 0 }, + { Zuint8, Cx0300, 0x0017, Z_(Primary2Intensity), Cm1, 0 }, + { Zuint16, Cx0300, 0x0019, Z_(Primary3X), Cm1, 0 }, + { Zuint16, Cx0300, 0x001A, Z_(Primary3Y), Cm1, 0 }, + { Zuint8, Cx0300, 0x001B, Z_(Primary3Intensity), Cm1, 0 }, + { Zuint16, Cx0300, 0x0030, Z_(WhitePointX), Cm1, 0 }, + { Zuint16, Cx0300, 0x0031, Z_(WhitePointY), Cm1, 0 }, + { Zuint16, Cx0300, 0x0032, Z_(ColorPointRX), Cm1, 0 }, + { Zuint16, Cx0300, 0x0033, Z_(ColorPointRY), Cm1, 0 }, + { Zuint8, Cx0300, 0x0034, Z_(ColorPointRIntensity), Cm1, 0 }, + { Zuint16, Cx0300, 0x0036, Z_(ColorPointGX), Cm1, 0 }, + { Zuint16, Cx0300, 0x0037, Z_(ColorPointGY), Cm1, 0 }, + { Zuint8, Cx0300, 0x0038, Z_(ColorPointGIntensity), Cm1, 0 }, + { Zuint16, Cx0300, 0x003A, Z_(ColorPointBX), Cm1, 0 }, + { Zuint16, Cx0300, 0x003B, Z_(ColorPointBY), Cm1, 0 }, + { Zuint8, Cx0300, 0x003C, Z_(ColorPointBIntensity), Cm1, 0 }, + + // Illuminance Measurement cluster + { Zuint16, Cx0400, 0x0000, Z_(Illuminance), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_PIR, illuminance) }, // Illuminance (in Lux) + { Zuint16, Cx0400, 0x0001, Z_(IlluminanceMinMeasuredValue), Cm1, 0 }, // + { Zuint16, Cx0400, 0x0002, Z_(IlluminanceMaxMeasuredValue), Cm1, 0 }, // + { Zuint16, Cx0400, 0x0003, Z_(IlluminanceTolerance), Cm1, 0 }, // + { Zenum8, Cx0400, 0x0004, Z_(IlluminanceLightSensorType), Cm1, 0 }, // + { Zunk, Cx0400, 0xFFFF, Z_(), Cm0, 0 }, // Remove all other values + + // Illuminance Level Sensing cluster + { Zenum8, Cx0401, 0x0000, Z_(IlluminanceLevelStatus), Cm1, 0 }, // Illuminance (in Lux) + { Zenum8, Cx0401, 0x0001, Z_(IlluminanceLightSensorType), Cm1, 0 }, // LightSensorType + { Zuint16, Cx0401, 0x0010, Z_(IlluminanceTargetLevel), Cm1, 0 }, // + { Zunk, Cx0401, 0xFFFF, Z_(), Cm0, 0 }, // Remove all other values + + // Temperature Measurement cluster + { Zint16, Cx0402, 0x0000, Z_(Temperature), Cm_100 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_Thermo, temperature) }, + { Zint16, Cx0402, 0x0001, Z_(TemperatureMinMeasuredValue), Cm_100, 0 }, // + { Zint16, Cx0402, 0x0002, Z_(TemperatureMaxMeasuredValue), Cm_100, 0 }, // + { Zuint16, Cx0402, 0x0003, Z_(TemperatureTolerance), Cm_100, 0 }, // + { Zunk, Cx0402, 0xFFFF, Z_(), Cm0, 0 }, // Remove all other values + + // Pressure Measurement cluster + { Zint16, Cx0403, 0x0000, Z_(Pressure), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_Thermo, pressure) }, // Pressure + { Zint16, Cx0403, 0x0001, Z_(PressureMinMeasuredValue), Cm1, 0 }, // + { Zint16, Cx0403, 0x0002, Z_(PressureMaxMeasuredValue), Cm1, 0 }, // + { Zuint16, Cx0403, 0x0003, Z_(PressureTolerance), Cm1, 0 }, // + { Zint16, Cx0403, 0x0010, Z_(PressureScaledValue), Cm1, 0 }, // + { Zint16, Cx0403, 0x0011, Z_(PressureMinScaledValue), Cm1, 0 }, // + { Zint16, Cx0403, 0x0012, Z_(PressureMaxScaledValue), Cm1, 0 }, // + { Zuint16, Cx0403, 0x0013, Z_(PressureScaledTolerance), Cm1, 0 }, // + { Zint8, Cx0403, 0x0014, Z_(PressureScale), Cm1, 0 }, // + { Zint16, Cx0403, 0xFFF0, Z_(SeaPressure), Cm1, Z_MAPPING(Z_Data_Thermo, pressure) }, // Pressure at Sea Level, Tasmota specific + { Zunk, Cx0403, 0xFFFF, Z_(), Cm0, 0 }, // Remove all other Pressure values + + // Flow Measurement cluster + { Zuint16, Cx0404, 0x0000, Z_(FlowRate), Cm_10, 0 }, // Flow (in m3/h) + { Zuint16, Cx0404, 0x0001, Z_(FlowMinMeasuredValue), Cm1, 0 }, // + { Zuint16, Cx0404, 0x0002, Z_(FlowMaxMeasuredValue), Cm1, 0 }, // + { Zuint16, Cx0404, 0x0003, Z_(FlowTolerance), Cm1, 0 }, // + { Zunk, Cx0404, 0xFFFF, Z_(), Cm0, 0 }, // Remove all other values + + // Relative Humidity Measurement cluster + { Zuint16, Cx0405, 0x0000, Z_(Humidity), Cm_100 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_Thermo, humidity) }, // Humidity + { Zuint16, Cx0405, 0x0001, Z_(HumidityMinMeasuredValue), Cm1, 0 }, // + { Zuint16, Cx0405, 0x0002, Z_(HumidityMaxMeasuredValue), Cm1, 0 }, // + { Zuint16, Cx0405, 0x0003, Z_(HumidityTolerance), Cm1, 0 }, // + { Zunk, Cx0405, 0xFFFF, Z_(), Cm0, 0 }, // Remove all other values + + // Occupancy Sensing cluster + { Zmap8, Cx0406, 0x0000, Z_(Occupancy), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_PIR, occupancy) }, // Occupancy (map8) + { Zenum8, Cx0406, 0x0001, Z_(OccupancySensorType), Cm1, 0 }, // OccupancySensorType + { Zunk, Cx0406, 0xFFFF, Z_(), Cm0, 0 }, // Remove all other values + + // IAS Cluster (Intruder Alarm System) + { Zenum8, Cx0500, 0x0000, Z_(ZoneState), Cm1, 0 }, // Occupancy (map8) + { Zenum16, Cx0500, 0x0001, Z_(ZoneType), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_Alarm, zone_type) }, // Zone type for sensor + { Zmap16, Cx0500, 0x0002, Z_(ZoneStatus), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_Alarm, zone_status) }, // Zone status for sensor + { Zuint8, Cx0500, 0xFFF0 + ZA_CIE, Z_(CIE), Cm1, 0 }, + { Zuint8, Cx0500, 0xFFF0 + ZA_PIR, Z_(Occupancy), Cm1, 0 }, // normally converted to the actual Occupancy 0406/0000 + { Zuint8, Cx0500, 0xFFF0 + ZA_Contact, Z_(Contact), Cm1, Z_MAPPING(Z_Data_Alarm, zone_status) }, // We fit the first bit in the LSB + { Zuint8, Cx0500, 0xFFF0 + ZA_Fire, Z_(Fire), Cm1, 0 }, + { Zuint8, Cx0500, 0xFFF0 + ZA_Water, Z_(Water), Cm1, 0 }, + { Zuint8, Cx0500, 0xFFF0 + ZA_CO, Z_(CO), Cm1, 0 }, + { Zuint8, Cx0500, 0xFFF0 + ZA_Personal, Z_(PersonalAlarm),Cm1, 0 }, + { Zuint8, Cx0500, 0xFFF0 + ZA_Movement, Z_(Movement), Cm1, 0 }, + { Zuint8, Cx0500, 0xFFF0 + ZA_Panic, Z_(Panic), Cm1, 0 }, + { Zuint8, Cx0500, 0xFFF0 + ZA_GlassBreak, Z_(GlassBreak),Cm1, 0 }, + + // Metering (Smart Energy) cluster + { Zuint48, Cx0702, 0x0000, Z_(EnergyTotal), Cm1, 0 }, + + // Meter Identification cluster + { Zstring, Cx0B01, 0x0000, Z_(CompanyName), Cm1, 0 }, + { Zuint16, Cx0B01, 0x0001, Z_(MeterTypeID), Cm1, 0 }, + { Zuint16, Cx0B01, 0x0004, Z_(DataQualityID), Cm1, 0 }, + { Zstring, Cx0B01, 0x0005, Z_(CustomerName), Cm1, 0 }, + { Zoctstr, Cx0B01, 0x0006, Z_(Model), Cm1, 0 }, + { Zoctstr, Cx0B01, 0x0007, Z_(PartNumber), Cm1, 0 }, + { Zoctstr, Cx0B01, 0x0008, Z_(ProductRevision), Cm1, 0 }, + { Zoctstr, Cx0B01, 0x000A, Z_(SoftwareRevision), Cm1, 0 }, + { Zstring, Cx0B01, 0x000B, Z_(UtilityName), Cm1, 0 }, + { Zstring, Cx0B01, 0x000C, Z_(POD), Cm1, 0 }, + { Zint24, Cx0B01, 0x000D, Z_(AvailablePower), Cm1, 0 }, + { Zint24, Cx0B01, 0x000E, Z_(PowerThreshold), Cm1, 0 }, + + // Electrical Measurement cluster + { Zuint16, Cx0B04, 0x0505, Z_(RMSVoltage), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_Plug, mains_voltage) }, + { Zuint16, Cx0B04, 0x0508, Z_(RMSCurrent), Cm1, 0 }, + { Zint16, Cx0B04, 0x050B, Z_(ActivePower), Cm1 + Z_EXPORT_DATA, Z_MAPPING(Z_Data_Plug, mains_power) }, + { Zint16, Cx0B04, 0x050E, Z_(ReactivePower), Cm1, 0 }, + { Zint16, Cx0B04, 0x050F, Z_(ApparentPower), Cm1, 0 }, + + // Diagnostics cluster + { Zuint16, Cx0B05, 0x0000, Z_(NumberOfResets), Cm1, 0 }, + { Zuint16, Cx0B05, 0x0001, Z_(PersistentMemoryWrites),Cm1, 0 }, + { Zuint8, Cx0B05, 0x011C, Z_(LastMessageLQI), Cm1, 0 }, + { Zuint8, Cx0B05, 0x011D, Z_(LastMessageRSSI), Cm1, 0 }, + + // Tuya Moes specific - 0xEF00 + // Mapping of Tuya type with internal mapping + // 0x00 - Zoctstr (len N) + // 0x01 - Ztuya1 (len 1) - equivalent to Zuint8 without invalid value handling + // 0x02 - Ztuya4 (len 4) - equivalent to Zint32 in big endian and without invalid value handling + // 0x03 - Zstr (len N) + // 0x04 - Ztuya1 (len 1) + // 0x05 - Ztuya4u (len 1/2/4) - equivalent to Zuint32 + // { Ztuya0, CxEF00, 0x0070, Z_(TuyaScheduleWorkdays), Cm1, 0 }, + // { Ztuya0, CxEF00, 0x0071, Z_(TuyaScheduleHolidays), Cm1, 0 }, + // { Ztuya1, CxEF00, 0x0101, Z_(Power), Cm1, 0 }, + // { Ztuya1, CxEF00, 0x0102, Z_(Power2), Cm1, 0 }, + // { Ztuya1, CxEF00, 0x0103, Z_(Power3), Cm1, 0 }, + // { Ztuya1, CxEF00, 0x0104, Z_(Power4), Cm1, 0 }, + // { Ztuya1, CxEF00, 0x0107, Z_(TuyaChildLock), Cm1, 0 }, + // { Ztuya1, CxEF00, 0x0112, Z_(TuyaWindowDetection), Cm1, 0 }, + // { Ztuya1, CxEF00, 0x0114, Z_(TuyaValveDetection), Cm1, 0 }, + // { Ztuya1, CxEF00, 0x0174, Z_(TuyaAutoLock), Cm1, 0 }, + // { Zint16, CxEF00, 0x0202, Z_(TuyaTempTarget), Cm_10, Z_MAPPING(Z_Data_Thermo, temperature_target) }, + // { Zint16, CxEF00, 0x0203, Z_(LocalTemperature), Cm_10, Z_MAPPING(Z_Data_Thermo, temperature) }, // will be overwritten by actual LocalTemperature + // { Zuint8, CxEF00, 0x0203, Z_(Dimmer), Cm1, Z_MAPPING(Z_Data_Light, dimmer) }, // will be overwritten by actual LocalTemperature + // { Zmap8, CxEF00, 0x0203, Z_(Occupancy), Cm1, Z_MAPPING(Z_Data_PIR, occupancy) }, // will be overwritten by actual LocalTemperature + // { Ztuya2, CxEF00, 0x0215, Z_(TuyaBattery), Cm1, 0 }, // TODO check equivalent? + // { Ztuya2, CxEF00, 0x0266, Z_(TuyaMinTemp), Cm1, 0 }, + // { Ztuya2, CxEF00, 0x0267, Z_(TuyaMaxTemp), Cm1, 0 }, + // { Ztuya2, CxEF00, 0x0269, Z_(TuyaBoostTime), Cm1, 0 }, + // { Ztuya2, CxEF00, 0x026B, Z_(TuyaComfortTemp), Cm1, 0 }, + // { Ztuya2, CxEF00, 0x026C, Z_(TuyaEcoTemp), Cm1, 0 }, + // { Zuint8, CxEF00, 0x026D, Z_(TuyaValvePosition), Cm1, Z_MAPPING(Z_Data_Thermo, th_setpoint) }, + // { Ztuya2, CxEF00, 0x0272, Z_(TuyaAwayTemp), Cm1, 0 }, + // { Ztuya2, CxEF00, 0x0275, Z_(TuyaAwayDays), Cm1, 0 }, + // { Ztuya4, CxEF00, 0x0404, Z_(TuyaPreset), Cm1, 0 }, + // { Ztuya4, CxEF00, 0x0405, Z_(TuyaFanMode), Cm1, 0 }, + // { Ztuya4, CxEF00, 0x046A, Z_(TuyaForceMode), Cm1, 0 }, + // { Ztuya4, CxEF00, 0x046F, Z_(TuyaWeekSelect), Cm1, 0 }, + + // Legrand BTicino - Manuf code 0x1021 + { Zdata16, CxFC01, 0x0000, Z_(LegrandOpt1), Cm1, 0 }, + { Zbool, CxFC01, 0x0001, Z_(LegrandOpt2), Cm1, 0 }, + { Zbool, CxFC01, 0x0002, Z_(LegrandOpt3), Cm1, 0 }, + + // Legrand - Manuf code 0x1021 + { Zenum8, CxFC40, 0x0000, Z_(LegrandHeatingMode), Cm1, 0 }, + + // Aqara Opple spacific + { Zuint8, CxFCC0, 0x0009, Z_(OppleMode), Cm1, 0 }, + + // Terncy specific - 0xFCCC + { Zuint16, CxFCCC, 0x001A, Z_(TerncyDuration), Cm1, 0 }, + { Zint16, CxFCCC, 0x001B, Z_(TerncyRotate), Cm1, 0 }, +}; +#pragma GCC diagnostic pop + +#endif // USE_ZIGBEE diff --git a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_5_converters.ino b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_5_2_converters.ino similarity index 58% rename from tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_5_converters.ino rename to tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_5_2_converters.ino index 94095188d..85faa6d85 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_5_converters.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_5_2_converters.ino @@ -19,653 +19,6 @@ #ifdef USE_ZIGBEE -/*********************************************************************************************\ - * ZCL -\*********************************************************************************************/ - - -enum Z_DataTypes { - Znodata = 0x00, - Zdata8 = 0x08, Zdata16, Zdata24, Zdata32, Zdata40, Zdata48, Zdata56, Zdata64, - Zbool = 0x10, - Zmap8 = 0x18, Zmap16, Zmap24, Zmap32, Zmap40, Zmap48, Zmap56, Zmap64, - Zuint8 = 0x20, Zuint16, Zuint24, Zuint32, Zuint40, Zuint48, Zuint56, Zuint64, - Zint8 = 0x28, Zint16, Zint24, Zint32, Zint40, Zint48, Zint56, Zint64, - Zenum8 = 0x30, Zenum16 = 0x31, - Zsemi = 0x38, Zsingle = 0x39, Zdouble = 0x3A, - Zoctstr = 0x41, Zstring = 0x42, Zoctstr16 = 0x43, Zstring16 = 0x44, - Arrray = 0x48, - Zstruct = 0x4C, - Zset = 0x50, Zbag = 0x51, - ZToD = 0xE0, Zdate = 0xE1, ZUTC = 0xE2, - ZclusterId = 0xE8, ZattribId = 0xE9, ZbacOID = 0xEA, - ZEUI64 = 0xF0, Zkey128 = 0xF1, - Zunk = 0xFF, - // adding fake type for Tuya specific encodings - Ztuya0 = Zoctstr, - Ztuya1 = Zbool, - Ztuya2 = Zint32, - Ztuya3 = Zstring, - Ztuya4 = Zuint8, - Ztuya5 = Zuint32 -}; - -// -// get the lenth in bytes for a data-type -// return 0 if unknown of type specific -// -// Note: this code is smaller than a static array -uint8_t Z_getDatatypeLen(uint8_t t) { - if ( ((t >= 0x08) && (t <= 0x0F)) || // data8 - data64 - ((t >= 0x18) && (t <= 0x2F)) ) { // map/uint/int - return (t & 0x07) + 1; - } - switch (t) { - case Zbool: - case Zenum8: - return 1; - case Zenum16: - case Zsemi: - case ZclusterId: - case ZattribId: - return 2; - case Zsingle: - case ZToD: - case Zdate: - case ZUTC: - case ZbacOID: - return 4; - case Zdouble: - case ZEUI64: - return 8; - case Zkey128: - return 16; - case Znodata: - default: - return 0; - } -} - -// is the type a discrete type, cf. section 2.6.2 of ZCL spec -bool Z_isDiscreteDataType(uint8_t t) { - if ( ((t >= 0x20) && (t <= 0x2F)) || // uint8 - int64 - ((t >= 0x38) && (t <= 0x3A)) || // semi - double - ((t >= 0xE0) && (t <= 0xE2)) ) { // ToD - UTC - return false; - } else { - return true; - } -} - -typedef struct Z_AttributeConverter { - uint8_t type; - uint8_t cluster_short; - uint16_t attribute; - uint16_t name_offset; - uint8_t multiplier_idx; // multiplier index for numerical value, use CmToMultiplier(), (if > 0 multiply by x, if <0 device by x) - // the high 4 bits are used to encode flags - // currently: 0x80 = this parameter needs to be exported to ZbData - uint8_t mapping; // high 4 bits = type, low 4 bits = offset in bytes from header - // still room for a byte -} Z_AttributeConverter; - -// Get offset in bytes of attributes, starting after the header (skipping first 4 bytes) -#define Z_OFFSET(c,a) (offsetof(class c, a) - sizeof(Z_Data)) -#define Z_CLASS(c) c // necessary to get a valid token without concatenation (which wouldn't work) -#define Z_MAPPING(c,a) (((((uint8_t)Z_CLASS(c)::type) & 0x0F) << 4) | Z_OFFSET(c,a)) - -// lines with this marker, will be used to export automatically data to `ZbData` -// at the condition Z_MAPPING() is also used -const uint8_t Z_EXPORT_DATA = 0x80; - -// Cluster numbers are store in 8 bits format to save space, -// the following tables allows the conversion from 8 bits index Cx... -// to the 16 bits actual cluster number -enum Cx_cluster_short { - Cx0000, Cx0001, Cx0002, Cx0003, Cx0004, Cx0005, Cx0006, Cx0007, - Cx0008, Cx0009, Cx000A, Cx000B, Cx000C, Cx000D, Cx000E, Cx000F, - Cx0010, Cx0011, Cx0012, Cx0013, Cx0014, Cx001A, Cx0020, Cx0100, - Cx0101, Cx0102, Cx0201, Cx0300, Cx0400, Cx0401, Cx0402, Cx0403, - Cx0404, Cx0405, Cx0406, Cx0500, Cx0702, Cx0B01, Cx0B04, Cx0B05, - CxEF00, CxFC01, CxFC40, CxFCC0, CxFCCC, -}; - -const uint16_t Cx_cluster[] PROGMEM = { - 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, - 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, - 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x001A, 0x0020, 0x0100, - 0x0101, 0x0102, 0x0201, 0x0300, 0x0400, 0x0401, 0x0402, 0x0403, - 0x0404, 0x0405, 0x0406, 0x0500, 0x0702, 0x0B01, 0x0B04, 0x0B05, - 0xEF00, 0xFC01, 0xFC40, 0xFCC0, 0xFCCC, -}; - -uint16_t CxToCluster(uint8_t cx) { - if (cx < nitems(Cx_cluster)) { - return pgm_read_word(&Cx_cluster[cx]); - } - return 0xFFFF; -} - -uint8_t ClusterToCx(uint16_t cluster) { - for (uint32_t i=0; iname_offset)) { continue; } // avoid strcasecmp_P() from crashing - if (0 == strcasecmp_P(command, Z_strings + pgm_read_word(&converter->name_offset))) { - if (cluster) { *cluster = CxToCluster(pgm_read_byte(&converter->cluster_short)); } - if (attribute) { *attribute = pgm_read_word(&converter->attribute); } - if (multiplier) { *multiplier = CmToMultiplier(pgm_read_byte(&converter->multiplier_idx)); } - if (zigbee_type) { *zigbee_type = pgm_read_byte(&converter->type); } - uint8_t conv_mapping = pgm_read_byte(&converter->mapping); - if (data_type) { *data_type = (Z_Data_Type) ((conv_mapping & 0xF0)>>4); } - if (map_offset) { *map_offset = (conv_mapping & 0x0F); } - return (const __FlashStringHelper*) (Z_strings + pgm_read_word(&converter->name_offset)); +// +// Find attribute matcher by name +// +class Z_attribute_match Z_findAttributeMatcherByName(uint16_t shortaddr, const char *name) { + const Z_Device & device = zigbee_devices.findShortAddr(shortaddr); + // works even if the device is unknown + + Z_attribute_match matched_attr = Z_plugin_matchAttributeByName(device.modelId, device.manufacturerId, name); + if (!matched_attr.found()) { + for (uint32_t i = 0; i < nitems(Z_PostProcess); i++) { + const Z_AttributeConverter *converter = &Z_PostProcess[i]; + if (0 == pgm_read_word(&converter->name_offset)) { continue; } // avoid strcasecmp_P() from crashing + if (0 == strcasecmp_P(name, Z_strings + pgm_read_word(&converter->name_offset))) { + matched_attr.cluster = CxToCluster(pgm_read_byte(&converter->cluster_short)); + matched_attr.attribute = pgm_read_word(&converter->attribute); + matched_attr.name = (Z_strings + pgm_read_word(&converter->name_offset)); + matched_attr.multiplier = CmToMultiplier(pgm_read_byte(&converter->multiplier_idx)); + matched_attr.zigbee_type = pgm_read_byte(&converter->type); + uint8_t conv_mapping = pgm_read_byte(&converter->mapping); + matched_attr.map_type = (Z_Data_Type) ((conv_mapping & 0xF0)>>4); + matched_attr.map_offset = (conv_mapping & 0x0F); + break; + } } } - return nullptr; + return matched_attr; } // -// Find attribute details: Name, Type, Multiplier by cuslter/attr_id +// Find attribute matcher by name // -const __FlashStringHelper* zigbeeFindAttributeById(uint16_t cluster, uint16_t attr_id, - uint8_t *attr_type, int8_t *multiplier) { - for (uint32_t i = 0; i < nitems(Z_PostProcess); i++) { - const Z_AttributeConverter *converter = &Z_PostProcess[i]; - uint16_t conv_cluster = CxToCluster(pgm_read_byte(&converter->cluster_short)); - uint16_t conv_attr_id = pgm_read_word(&converter->attribute); +// attr_glob: do Attr marked as 0xFFFF match any attribute +class Z_attribute_match Z_findAttributeMatcherById(uint16_t shortaddr, uint16_t cluster, uint16_t attr_id, bool attr_glob) { + const Z_Device & device = zigbee_devices.findShortAddr(shortaddr); + // works even if the device is unknown - if ((conv_cluster == cluster) && (conv_attr_id == attr_id)) { - if (multiplier) { *multiplier = CmToMultiplier(pgm_read_byte(&converter->multiplier_idx)); } - if (attr_type) { *attr_type = pgm_read_byte(&converter->type); } - return (const __FlashStringHelper*) (Z_strings + pgm_read_word(&converter->name_offset)); + Z_attribute_match matched_attr = Z_plugin_matchAttributeById(device.modelId, device.manufacturerId, cluster, attr_id); + if (!matched_attr.found()) { + for (uint32_t i = 0; i < nitems(Z_PostProcess); i++) { + const Z_AttributeConverter *converter = &Z_PostProcess[i]; + uint16_t conv_cluster = CxToCluster(pgm_read_byte(&converter->cluster_short)); + uint16_t conv_attr_id = pgm_read_word(&converter->attribute); + + if ((conv_cluster == cluster) && (conv_attr_id == attr_id || (attr_glob && conv_attr_id == 0xFFFF))) { + matched_attr.cluster = cluster; + matched_attr.attribute = attr_id; + matched_attr.name = (Z_strings + pgm_read_word(&converter->name_offset)); + matched_attr.multiplier = CmToMultiplier(pgm_read_byte(&converter->multiplier_idx)); + matched_attr.zigbee_type = pgm_read_byte(&converter->type); + uint8_t conv_mapping = pgm_read_byte(&converter->mapping); + matched_attr.map_type = (Z_Data_Type) ((conv_mapping & 0xF0)>>4); + matched_attr.map_offset = (conv_mapping & 0x0F); + break; + } } } - return nullptr; + return matched_attr; } class ZCLFrame { @@ -801,10 +171,10 @@ public: void removeInvalidAttributes(Z_attribute_list& attr_list); void computeSyntheticAttributes(Z_attribute_list& attr_list); void generateCallBacks(Z_attribute_list& attr_list); - void parseReadAttributes(Z_attribute_list& attr_list); + void parseReadAttributes(uint16_t shortaddr, Z_attribute_list& attr_list); void parseReadAttributesResponse(Z_attribute_list& attr_list); - void parseReadConfigAttributes(Z_attribute_list& attr_list); - void parseConfigAttributes(Z_attribute_list& attr_list); + void parseReadConfigAttributes(uint16_t shortaddr, Z_attribute_list& attr_list); + void parseConfigAttributes(uint16_t shortaddr, Z_attribute_list& attr_list); void parseWriteAttributesResponse(Z_attribute_list& attr_list); void parseResponse(void); void parseResponse_inner(uint8_t cmd, bool cluster_specific, uint8_t status); @@ -1316,11 +686,26 @@ void ZCLFrame::removeInvalidAttributes(Z_attribute_list& attr_list) { // void ZCLFrame::computeSyntheticAttributes(Z_attribute_list& attr_list) { const Z_Device & device = zigbee_devices.findShortAddr(shortaddr); - const char * model_c = zigbee_devices.getModelId(shortaddr); // null if unknown - String modelId((char*) model_c); + + String modelId((char*) device.modelId); // scan through attributes and apply specific converters for (auto &attr : attr_list) { if (attr.key_is_str) { continue; } // pass if key is a name + + // first apply synonyms from plugins + Z_attribute_synonym syn = Z_plugin_matchAttributeSynonym(device.modelId, device.manufacturerId, + attr.key.id.cluster, attr.key.id.attr_id); + if (syn.found()) { + attr.setKeyId(syn.new_cluster, syn.new_attribute); + if (syn.multiplier != 1 && syn.multiplier != 0) { + // we need to change the value + float fval = attr.getFloat(); + if (syn.multiplier > 0) { fval = fval * syn.multiplier; } + else { fval = fval / (-syn.multiplier); } + attr.setFloat(fval); + } + } + uint32_t ccccaaaa = (attr.key.id.cluster << 16) | attr.key.id.attr_id; switch (ccccaaaa) { // 0xccccaaaa . c=cluster, a=attribute @@ -1478,7 +863,7 @@ void sendHueUpdate(uint16_t shortaddr, uint16_t groupaddr, uint16_t cluster, uin } // ZCL_READ_ATTRIBUTES -void ZCLFrame::parseReadAttributes(Z_attribute_list& attr_list) { +void ZCLFrame::parseReadAttributes(uint16_t shortaddr, Z_attribute_list& attr_list) { uint32_t i = 0; uint32_t len = payload.len(); @@ -1494,15 +879,9 @@ void ZCLFrame::parseReadAttributes(Z_attribute_list& attr_list) { read_attr_ids[i/2] = attrid; // find the attribute name - for (uint32_t i = 0; i < nitems(Z_PostProcess); i++) { - const Z_AttributeConverter *converter = &Z_PostProcess[i]; - uint16_t conv_cluster = CxToCluster(pgm_read_byte(&converter->cluster_short)); - uint16_t conv_attribute = pgm_read_word(&converter->attribute); - - if ((conv_cluster == cluster) && (conv_attribute == attrid)) { - attr_names.addAttribute(Z_strings + pgm_read_word(&converter->name_offset), true).setBool(true); - break; - } + Z_attribute_match matched_attr = Z_findAttributeMatcherById(shortaddr, cluster, attrid, false); + if (matched_attr.found()) { + attr_names.addAttribute(matched_attr.name, true).setBool(true); } i += 2; } @@ -1516,7 +895,7 @@ void ZCLFrame::parseReadAttributes(Z_attribute_list& attr_list) { } // ZCL_CONFIGURE_REPORTING_RESPONSE -void ZCLFrame::parseConfigAttributes(Z_attribute_list& attr_list) { +void ZCLFrame::parseConfigAttributes(uint16_t shortaddr, Z_attribute_list& attr_list) { uint32_t len = payload.len(); Z_attribute_list attr_config_list; @@ -1528,8 +907,9 @@ void ZCLFrame::parseConfigAttributes(Z_attribute_list& attr_list) { attr_config_response.addAttributePMEM(PSTR("Status")).setUInt(status); attr_config_response.addAttributePMEM(PSTR("StatusMsg")).setStr(getZigbeeStatusMessage(status).c_str()); - const __FlashStringHelper* attr_name = zigbeeFindAttributeById(cluster, attr_id, nullptr, nullptr); - if (attr_name) { + Z_attribute_match attr_matched = Z_findAttributeMatcherById(shortaddr, cluster, attr_id, false); + if (attr_matched.found()) { + const __FlashStringHelper* attr_name = (const __FlashStringHelper*) attr_matched.name; attr_config_list.addAttribute(attr_name).setStrRaw(attr_config_response.toString(true).c_str()); } else { attr_config_list.addAttribute(cluster, attr_id).setStrRaw(attr_config_response.toString(true).c_str()); @@ -1546,7 +926,7 @@ void ZCLFrame::parseWriteAttributesResponse(Z_attribute_list& attr_list) { } // ZCL_READ_REPORTING_CONFIGURATION_RESPONSE -void ZCLFrame::parseReadConfigAttributes(Z_attribute_list& attr_list) { +void ZCLFrame::parseReadConfigAttributes(uint16_t shortaddr, Z_attribute_list& attr_list) { uint32_t i = 0; uint32_t len = payload.len(); @@ -1563,19 +943,12 @@ void ZCLFrame::parseReadConfigAttributes(Z_attribute_list& attr_list) { attr_2.addAttributePMEM(PSTR("DirectionReceived")).setBool(true); } - // find the attribute name + // find the multiplier int8_t multiplier = 1; - for (uint32_t i = 0; i < nitems(Z_PostProcess); i++) { - const Z_AttributeConverter *converter = &Z_PostProcess[i]; - uint16_t conv_cluster = CxToCluster(pgm_read_byte(&converter->cluster_short)); - uint16_t conv_attribute = pgm_read_word(&converter->attribute); - - if ((conv_cluster == cluster) && (conv_attribute == attrid)) { - const char * attr_name = Z_strings + pgm_read_word(&converter->name_offset); - attr_2.addAttribute(attr_name, true).setBool(true); - multiplier = CmToMultiplier(pgm_read_byte(&converter->multiplier_idx)); - break; - } + Z_attribute_match matched_attr = Z_findAttributeMatcherById(shortaddr, cluster, attrid, false); + if (matched_attr.found()) { + attr_2.addAttribute(matched_attr.name, true).setBool(true); + multiplier = matched_attr.multiplier; } i += 4; if (0 != status) { @@ -2034,31 +1407,13 @@ void Z_postProcessAttributes(uint16_t shortaddr, uint16_t src_ep, class Z_attrib // Look for an entry in the converter table bool found = false; - const char * conv_name; - Z_Data_Type map_type = Z_Data_Type::Z_Unknown; - uint8_t map_offset = 0; - uint8_t zigbee_type = Znodata; - int8_t conv_multiplier; - for (uint32_t i = 0; i < nitems(Z_PostProcess); i++) { - const Z_AttributeConverter *converter = &Z_PostProcess[i]; - uint16_t conv_cluster = CxToCluster(pgm_read_byte(&converter->cluster_short)); - uint16_t conv_attribute = pgm_read_word(&converter->attribute); - if ((conv_cluster == cluster) && - ((conv_attribute == attribute) || (conv_attribute == 0xFFFF)) ) { - conv_multiplier = CmToMultiplier(pgm_read_byte(&converter->multiplier_idx)); - zigbee_type = pgm_read_byte(&converter->type); - uint8_t mapping = pgm_read_byte(&converter->mapping); - map_type = (Z_Data_Type) ((mapping & 0xF0)>>4); - map_offset = (mapping & 0x0F); - conv_name = Z_strings + pgm_read_word(&converter->name_offset); - found = true; - break; - } - } + // first search in device plug-ins + const Z_attribute_match matched_attr = Z_findAttributeMatcherById(shortaddr, cluster, attribute, true); + found = matched_attr.found(); float fval = attr.getFloat(); - if (found && (map_type != Z_Data_Type::Z_Unknown)) { + if (found && (matched_attr.map_type != Z_Data_Type::Z_Unknown)) { // We apply an automatic mapping to Z_Data_XXX object // First we find or instantiate the correct Z_Data_XXX accorfing to the endpoint // Then store the attribute at the attribute addres (via offset) and according to size 8/16/32 bits @@ -2066,11 +1421,11 @@ void Z_postProcessAttributes(uint16_t shortaddr, uint16_t src_ep, class Z_attrib // add the endpoint if it was not already known device.addEndpoint(src_ep); // we don't apply the multiplier, but instead store in Z_Data_XXX object - Z_Data & data = device.data.getByType(map_type, src_ep); - uint8_t *attr_address = ((uint8_t*)&data) + sizeof(Z_Data) + map_offset; + Z_Data & data = device.data.getByType(matched_attr.map_type, src_ep); + uint8_t *attr_address = ((uint8_t*)&data) + sizeof(Z_Data) + matched_attr.map_offset; uint32_t uval32 = attr.getUInt(); // call converter to uint only once int32_t ival32 = attr.getInt(); // call converter to int only once - // AddLog(LOG_LEVEL_DEBUG_MORE, PSTR(D_LOG_ZIGBEE "Mapping type=%d offset=%d zigbee_type=%02X value=%d\n"), (uint8_t) map_type, map_offset, zigbee_type, ival32); + // AddLog(LOG_LEVEL_DEBUG_MORE, PSTR(D_LOG_ZIGBEE "Mapping type=%d offset=%d zigbee_type=%02X value=%d\n"), (uint8_t) matched_attr.matched_attr, matched_attr.map_offset, matched_attr.zigbee_type, ival32); switch (ccccaaaa) { case 0xEF000202: case 0xEF000203: // need to convert Tuya temperatures from 1/10 to 1/00 °C @@ -2078,7 +1433,7 @@ void Z_postProcessAttributes(uint16_t shortaddr, uint16_t src_ep, class Z_attrib break; } - switch (zigbee_type) { + switch (matched_attr.zigbee_type) { case Zenum8: case Zmap8: case Zbool: @@ -2124,18 +1479,18 @@ void Z_postProcessAttributes(uint16_t shortaddr, uint16_t src_ep, class Z_attrib // now apply the multiplier to make it human readable if (found) { - if (0 == conv_multiplier) { attr_list.removeAttribute(&attr); continue; } // remove attribute if multiplier is zero - if (1 != conv_multiplier) { - if (conv_multiplier > 0) { fval = fval * conv_multiplier; } - else { fval = fval / (-conv_multiplier); } + if (0 == matched_attr.multiplier) { attr_list.removeAttribute(&attr); continue; } // remove attribute if multiplier is zero + if (1 != matched_attr.multiplier) { + if (matched_attr.multiplier > 0) { fval = fval * matched_attr.multiplier; } + else { fval = fval / (-matched_attr.multiplier); } attr.setFloat(fval); } } // Replace cluster/attribute with name if (found) { - if (0x00 != pgm_read_byte(conv_name)) {// if name is not null, replace it - attr.setKeyName(conv_name, true); // PMEM string so no need to copy + if (0x00 != pgm_read_byte(matched_attr.name)) {// if name is not null, replace it + attr.setKeyName(matched_attr.name, true); // PMEM string so no need to copy } } } @@ -2143,35 +1498,25 @@ void Z_postProcessAttributes(uint16_t shortaddr, uint16_t src_ep, class Z_attrib } // Internal search function -void Z_parseAttributeKey_inner(class Z_attribute & attr, uint16_t preferred_cluster) { - // scan attributes to find by name, and retrieve type - for (uint32_t i = 0; i < nitems(Z_PostProcess); i++) { - const Z_AttributeConverter *converter = &Z_PostProcess[i]; - uint16_t local_attr_id = pgm_read_word(&converter->attribute); - uint16_t local_cluster_id = CxToCluster(pgm_read_byte(&converter->cluster_short)); - uint8_t local_type_id = pgm_read_byte(&converter->type); - int8_t local_multiplier = CmToMultiplier(pgm_read_byte(&converter->multiplier_idx)); - // AddLog(LOG_LEVEL_DEBUG, PSTR("Try cluster = 0x%04X, attr = 0x%04X, type_id = 0x%02X"), local_cluster_id, local_attr_id, local_type_id); - - if (!attr.key_is_str) { - if ((attr.key.id.cluster == local_cluster_id) && (attr.key.id.attr_id == local_attr_id)) { - attr.attr_type = local_type_id; - break; - } - } else if (pgm_read_word(&converter->name_offset)) { - const char * key = attr.key.key; - // AddLog(LOG_LEVEL_DEBUG, PSTR("Comparing '%s' with '%s'"), attr_name, converter->name); - if (0 == strcasecmp_P(key, Z_strings + pgm_read_word(&converter->name_offset))) { - if ((preferred_cluster == 0xFFFF) || // any cluster - (local_cluster_id == preferred_cluster)) { - // match - attr.setKeyId(local_cluster_id, local_attr_id); - attr.attr_type = local_type_id; - attr.attr_multiplier = local_multiplier; - break; - } +void Z_parseAttributeKey_inner(uint16_t shortaddr, class Z_attribute & attr, uint16_t preferred_cluster) { + if (attr.key_is_str) { + // find the attribute name + Z_attribute_match matched_attr = Z_findAttributeMatcherByName(shortaddr, attr.key.key); + if (matched_attr.found()) { + if ((preferred_cluster == 0xFFFF) || // any cluster + (matched_attr.cluster == preferred_cluster)) { + // match + attr.setKeyId(matched_attr.cluster, matched_attr.attribute); + attr.attr_type = matched_attr.zigbee_type; + attr.attr_multiplier = matched_attr.multiplier; } } + } else { + // find by cluster/attribute + Z_attribute_match matched_attr = Z_findAttributeMatcherById(shortaddr, attr.key.id.cluster, attr.key.id.attr_id, false); + if (matched_attr.found()) { + attr.attr_type = matched_attr.zigbee_type; + } } } @@ -2191,7 +1536,7 @@ void Z_parseAttributeKey_inner(class Z_attribute & attr, uint16_t preferred_clus // Note: the attribute value is unchanged and unparsed // // Note: if the type is specified in the key, the multiplier is not applied, no conversion happens -bool Z_parseAttributeKey(class Z_attribute & attr, uint16_t preferred_cluster) { +bool Z_parseAttributeKey(uint16_t shortaddr, class Z_attribute & attr, uint16_t preferred_cluster) { // check if the name has the format "XXXX/YYYY" where XXXX is the cluster, YYYY the attribute id // alternative "XXXX/YYYY%ZZ" where ZZ is the type (for unregistered attributes) if (attr.key_is_str) { @@ -2208,7 +1553,7 @@ bool Z_parseAttributeKey(class Z_attribute & attr, uint16_t preferred_cluster) { attr_id = strtoul(delimiter+1, nullptr, 16); } else { attr_id = strtoul(delimiter+1, &delimiter2, 16); - type_id = strtoul(delimiter2+1, nullptr, 16); + type_id = Z_getTypeByName(delimiter2+1); } attr.setKeyId(cluster_id, attr_id); attr.attr_type = type_id; @@ -2218,12 +1563,19 @@ bool Z_parseAttributeKey(class Z_attribute & attr, uint16_t preferred_cluster) { // do we already know the type, i.e. attribute and cluster are also known if ((Zunk == attr.attr_type) && (preferred_cluster != 0xFFFF)) { - Z_parseAttributeKey_inner(attr, preferred_cluster); // try to find with the selected cluster + Z_parseAttributeKey_inner(shortaddr, attr, preferred_cluster); // try to find with the selected cluster } if (Zunk == attr.attr_type) { - Z_parseAttributeKey_inner(attr, 0xFFFF); // try again with any cluster + Z_parseAttributeKey_inner(shortaddr, attr, 0xFFFF); // try again with any cluster } - return (Zunk != attr.attr_type) ? true : false; + // special case for Tuya attributes, where Zunk is allowed + if (Zunk == attr.attr_type) { + if (!attr.key_is_str && attr.key.id.cluster == 0xEF00) { + return true; + } + return false; // couldn't find any match + } + return true; } // generic toAttributes() based on declaration in the attribute array diff --git a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_6_commands.ino b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_6_commands.ino index f951e09e9..6990c60db 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_6_commands.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_6_commands.ino @@ -212,7 +212,6 @@ void Z_Unreachable(uint16_t shortaddr, uint16_t groupaddr, uint16_t cluster, uin zigbee_devices.getShortAddr(shortaddr).setReachable(false); // mark device as reachable Z_attribute_list attr_list; attr_list.addAttributePMEM(PSTR("Reachable")).setBool(false); // "Reachable":false - // Z_postProcessAttributes(shortaddr, endpoint, attr_list); // make sure all is updated accordingly zigbee_devices.jsonPublishNow(shortaddr, attr_list); } } diff --git a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_7_0_statemachine.ino b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_7_0_statemachine.ino index 13f301b29..eb7e2074a 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_7_0_statemachine.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_7_0_statemachine.ino @@ -529,6 +529,7 @@ static const Zigbee_Instruction zb_prog[] PROGMEM = { ZI_CALL(&Z_Load_Devices, 0) ZI_CALL(&Z_Load_Data, 0) ZI_CALL(&Z_Set_Save_Data_Timer, 0) + ZI_CALL(&Z_ZbAutoload, 0) ZI_CALL(&Z_Query_Bulbs, 0) ZI_LABEL(ZIGBEE_LABEL_MAIN_LOOP) @@ -966,6 +967,7 @@ static const Zigbee_Instruction zb_prog[] PROGMEM = { ZI_CALL(&Z_Load_Devices, 0) ZI_CALL(&Z_Load_Data, 0) ZI_CALL(&Z_Set_Save_Data_Timer, 0) + ZI_CALL(&Z_ZbAutoload, 0) ZI_CALL(&Z_Query_Bulbs, 0) ZI_LABEL(ZIGBEE_LABEL_MAIN_LOOP) diff --git a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_7_6_plugin.ino b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_7_6_plugin.ino new file mode 100644 index 000000000..a101513d2 --- /dev/null +++ b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_7_6_plugin.ino @@ -0,0 +1,379 @@ +/* + xdrv_23_zigbee.ino - zigbee support for Tasmota + + Copyright (C) 2021 Theo Arends and Stephan Hadinger + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifdef USE_ZIGBEE + +// global singleton +Z_plugin_templates g_plugin_templates; + +class Z_attribute_synonym Z_plugin_matchAttributeSynonym(const char *model, const char *manufacturer, uint16_t cluster, uint16_t attribute) { + const Z_attribute_synonym * attr_syn; + attr_syn = g_plugin_templates.matchAttributeSynonym(model == nullptr ? "" : model, + manufacturer == nullptr ? "" : manufacturer, + cluster, attribute); + + Z_attribute_synonym syn; + if (attr_syn != nullptr) { + syn = *attr_syn; + } + return syn; +} + +Z_attribute_match Z_plugin_matchAttributeById(const char *model, const char *manufacturer, uint16_t cluster, uint16_t attribute) { + const Z_plugin_attribute * attr_tmpl; + attr_tmpl = g_plugin_templates.matchAttributeById(model == nullptr ? "" : model, + manufacturer == nullptr ? "" : manufacturer, + cluster, attribute); + + Z_attribute_match attr; + if (attr_tmpl != nullptr) { + attr.cluster = attr_tmpl->cluster; + attr.attribute = attr_tmpl->attribute; + attr.name = attr_tmpl->name.c_str(); + attr.zigbee_type = attr_tmpl->type; + attr.multiplier = attr_tmpl->multiplier; + } + return attr; +} + +Z_attribute_match Z_plugin_matchAttributeByName(const char *model, const char *manufacturer, const char * name) { + const Z_plugin_attribute * attr_tmpl; + Z_attribute_match attr; + + if (name != nullptr) { + attr_tmpl = g_plugin_templates.matchAttributeByName(model == nullptr ? "" : model, + manufacturer == nullptr ? "" : manufacturer, + name); + + if (attr_tmpl != nullptr) { + attr.cluster = attr_tmpl->cluster; + attr.attribute = attr_tmpl->attribute; + attr.name = attr_tmpl->name.c_str(); + attr.zigbee_type = attr_tmpl->type; + attr.multiplier = attr_tmpl->multiplier; + } + } + return attr; +} + +bool Zb_readline(File *f, char* buf, size_t size) { + bool eof = 0; + while (1) { + // read line + bool comment = false; // did we encounter '#', if so ignore anything until '\n' + char * p = buf; + while (1) { + int c = f->read(); + if (c == -1) { eof = true; break; } // EOF reached + if (c == '#') { comment = true; } // rest of line is ignored + if (c == '\n') { break; } // end of line + if (!comment) { + if (p < buf + size - 1) { + *p++ = c; // append character + } else { + AddLog(LOG_LEVEL_INFO, "ZIG: ZbLoad line exceeds 96 bytes, aborting"); + return false; + } + } + } + int32_t ret = p - buf; // len in bytes + if (eof && ret == 0) { return false; } // nothing more to read + // found something, don't add the \n since we don't need it + buf[ret] = 0; // add string terminator + RemoveSpace(buf); // remove anything that looks like a space, tab, crlf... + // AddLog(LOG_LEVEL_INFO, "ZIG: ZbRead>'%s'", buf); + return true; + } +} + +#ifdef USE_UFILESYS +extern FS *ffsp; +#endif + +// load a file from filesystem +// returns `true` if success +bool ZbLoad(const char *filename_raw) { + +#ifdef USE_UFILESYS + if (ffsp) { + // first unload previsou definitions + ZbUnload(filename_raw); + + String filename = filename_raw; + if (filename_raw[0] != '/') { + filename = "/"; + filename += filename_raw; + } + File fp; + fp = ffsp->open(filename.c_str(), "r"); + + if (fp <= 0) { + AddLog(LOG_LEVEL_INFO, "ZIG: unable to load file '%s'", filename.c_str()); + return false; + } + + char * filename_imported = nullptr; + Z_plugin_template * tmpl = nullptr; // current template with matchers and attributes + bool new_matchers = false; // indicates that we have finished the current matchers + char buf_line[96]; // max line is 96 bytes (comments don't count) + + // read the first 6 chars + bool invalid_header = false; + static const char Z2T_HEADER_V1[] PROGMEM = "#Z2Tv1"; + for (uint32_t i = 0; i < 6; i++) { + int c = fp.read(); + if (c < 0) { + invalid_header = true; + break; + } + buf_line[i] = c; + buf_line[i+1] = 0; + } + if (!invalid_header) { + if (strcmp_P(buf_line, Z2T_HEADER_V1) != 0) { + invalid_header = true; + } + } + + if (invalid_header) { + AddLog(LOG_LEVEL_INFO, "ZIG: ZbLoad '%s' invalid header", filename_raw); + return false; + } + + // parse line by line + while (1) { + if (!Zb_readline(&fp, buf_line, sizeof(buf_line))) { + // EOF + break; + } + + // at first valid line, we instanciate a new plug-in instance and assign a filemane + if (filename_imported == nullptr) { + // allocate only once the filename for multiple entries + // freed only by `ZbUnload` + filename_imported = (char*) malloc(strlen(filename.c_str())+1); + strcpy(filename_imported, filename.c_str()); + } + + // there is a non-empty line, containing no space/tab/crlf + // depending on the first char, parse either device name or cluster/attribute+name + if (buf_line[0] == ':') { + if (tmpl == nullptr || new_matchers) { + tmpl = &g_plugin_templates.addToLast(); + tmpl->filename = filename_imported; + new_matchers = false; + } + // parse device name + char *rest = buf_line + 1; // skip first char ':' + char *token = strtok_r(rest, ",", &rest); + Z_plugin_matcher & matcher = tmpl->matchers.addToLast(); + if (token != nullptr) { + matcher.model = token; + } + token = strtok_r(rest, ",", &rest); + if (token != nullptr) { + matcher.manufacturer = token; + } + } else { + if (tmpl == nullptr) { + continue; + } + new_matchers = true; + char *rest = buf_line; + char *token = strtok_r(rest, ",", &rest); + if (token == nullptr) { + continue; + } + + // detect if token contains '=', if yes it is a synonym + char * delimiter_equal = strchr(token, '='); + + if (delimiter_equal == nullptr) { + // token is of from '0000/0000' or '0000/0000%00' + char * delimiter_slash = strchr(token, '/'); + char * delimiter_percent = strchr(token, '%'); + if (delimiter_slash == nullptr || (delimiter_percent != nullptr && delimiter_slash > delimiter_percent)) { + AddLog(LOG_LEVEL_INFO, "ZIG: ZbLoad '%s' wrong delimiter '%s'", filename_raw, token); + } + + uint16_t attr_id = 0xFFFF; + uint16_t cluster_id = 0xFFFF; + uint8_t type_id = Zunk; + + cluster_id = strtoul(token, &delimiter_slash, 16); + if (!delimiter_percent) { + attr_id = strtoul(delimiter_slash+1, nullptr, 16); + } else { + attr_id = strtoul(delimiter_slash+1, &delimiter_percent, 16); + type_id = Z_getTypeByName(delimiter_percent+1); + } + // name of the attribute + token = strtok_r(rest, ",", &rest); + if (token == nullptr) { + AddLog(LOG_LEVEL_INFO, "ZIG: ZbLoad '%s' ignore missing name '%s'", filename_raw, buf_line); + continue; + } + // token contains the name of the attribute + Z_plugin_attribute & plugin_attr = tmpl->attributes.addToLast(); + plugin_attr.cluster = cluster_id; + plugin_attr.attribute = attr_id; + plugin_attr.type = type_id; + plugin_attr.name = token; + } else { + // token is of from '0000/0000=0000/0000,1' + char * rest2 = token; + char * tok2 = strtok_r(rest2, "=", &rest2); + char * delimiter_slash = strchr(tok2, '/'); + uint16_t cluster_id = strtoul(tok2, &delimiter_slash, 16); + uint16_t attr_id = strtoul(delimiter_slash+1, nullptr, 16); + tok2 = strtok_r(rest2, "=", &rest2); + char * delimiter_slash2 = strchr(tok2, '/'); + uint16_t new_cluster_id = strtoul(tok2, &delimiter_slash2, 16); + uint16_t new_attr_id = strtoul(delimiter_slash2+1, nullptr, 16); + // multiplier + token = strtok_r(rest, ",", &rest); + int8_t multiplier = 1; + if (token != nullptr) { + multiplier = strtol(token, nullptr, 10); + } + // create the synonym + Z_attribute_synonym & syn = tmpl->synonyms.addToLast(); + syn.cluster = cluster_id; + syn.attribute = attr_id; + syn.new_cluster = new_cluster_id; + syn.new_attribute = new_attr_id; + syn.multiplier = multiplier; + } + } + } + } else { + AddLog(LOG_LEVEL_ERROR, "ZIG: filesystem not enabled"); + } +#else + AddLog(LOG_LEVEL_INFO, "ZIG: ZbLoad requires file system"); +#endif + return true; +} + +// Unlaod previously loaded definitions +bool ZbUnload(const char *filename_raw) { + String filename = filename_raw; + if (filename_raw[0] != '/') { + filename = "/"; + filename += filename_raw; + } + + char * filename_registered = nullptr; // internal allocation for filename + for (const Z_plugin_template & tmpl : g_plugin_templates) { + bool to_be_freed = false; + if (filename_registered) { + // if filename_registered is not NULL, compare pointers + if (tmpl.filename == filename_registered) { to_be_freed = true; } + } else { + if (strcmp(tmpl.filename, filename.c_str()) == 0) { + filename_registered = tmpl.filename; + to_be_freed = true; + } + } + // check if we remove this node + if (to_be_freed) { + g_plugin_templates.remove(&tmpl); + } + } + // free memory for filename + if (filename_registered) { + free(filename_registered); + AddLog(LOG_LEVEL_INFO, "ZIG: ZbUnload '%s' sucessful", filename_raw); + return true; + } + return false; +} + +// Dump the ZbLoad structure in a format compatible with ZbLoad +void ZbLoadDump(void) { + AddLog(LOG_LEVEL_INFO, "ZIG: ZbLoad dump all current information"); + AddLog(LOG_LEVEL_INFO, "====> START"); + + for (const Z_plugin_template & tmpl : g_plugin_templates) { + if (tmpl.filename != nullptr) { + AddLog(LOG_LEVEL_INFO, "# imported from '%s'", tmpl.filename); + } + // marchers + if (tmpl.matchers.length() == 0) { + AddLog(LOG_LEVEL_INFO, ": # no matcher"); + } else { + for (const Z_plugin_matcher & matcher : tmpl.matchers) { + AddLog(LOG_LEVEL_INFO, ":%s,%s", matcher.model.c_str(), matcher.manufacturer.c_str()); + } + } + // attributes + if (tmpl.attributes.length() == 0 && tmpl.synonyms.length() == 0) { + // no content, output an empty line + AddLog(LOG_LEVEL_INFO, ""); + } else { + for (const Z_plugin_attribute & attr : tmpl.attributes) { + if (attr.type == Zunk) { + AddLog(LOG_LEVEL_INFO, "%04X/%04X,%s", attr.cluster, attr.attribute, attr.name.c_str()); + } else { + char type[16]; + Z_getTypeByNumber(type, sizeof(type), attr.type); + AddLog(LOG_LEVEL_INFO, "%04X/%04X%%%s,%s", attr.cluster, attr.attribute, type, attr.name.c_str()); + } + } + for (const Z_attribute_synonym & syn : tmpl.synonyms) { + AddLog(LOG_LEVEL_INFO, "%04X/%04X=%04X/%04X,%i", syn.cluster, syn.attribute, syn.new_cluster, syn.new_attribute, syn.multiplier); + } + } + } + + AddLog(LOG_LEVEL_INFO, "<==== END"); +} + +// Auto-load all files ending with '.zb' +void ZbAutoload(void) { +#ifdef USE_UFILESYS + if (ffsp) { + File dir = ffsp->open("/", "r"); + if (dir) { + dir.rewindDirectory(); + while (1) { + File entry = dir.openNextFile(); + if (!entry) { break; } + const char * fn = entry.name(); + if (strcmp(fn, ".") && strcmp(fn, "..")) { + // check suffix + size_t l = strlen(fn); + if (l > 3) { + if (fn[l-3] == '.' && fn[l-2] == 'z' && fn[l-1] == 'b') { + bool ret = ZbLoad(fn); + if (ret) { + AddLog(LOG_LEVEL_INFO, "ZIG: ZbLoad '%s' loaded successfully", fn); + } + } + } + } + + } + + } + } +#endif // USE_UFILESYS +} + +#endif // USE_ZIGBEE diff --git a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_8_parsers.ino b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_8_parsers.ino index 14b64109a..3cc1eacb7 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_8_parsers.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_8_parsers.ino @@ -1553,25 +1553,24 @@ void Z_AutoConfigReportingForCluster(uint16_t shortaddr, uint16_t groupaddr, uin uint16_t max_interval = pgm_read_word(&(Z_autoAttributeReporting[i].max_interval)); float report_change_raw = Z_autoAttributeReporting[i].report_change; double report_change = report_change_raw; - uint8_t attr_type; - int8_t multiplier; + // uint8_t attr_type; + // int8_t multiplier; - const __FlashStringHelper* attr_name = zigbeeFindAttributeById(cluster, attr_id, &attr_type, &multiplier); - - if (attr_name) { + Z_attribute_match attr_matched = Z_findAttributeMatcherById(shortaddr, cluster, attr_id, false); + if (attr_matched.found()) { if (comma) { ResponseAppend_P(PSTR(",")); } comma = true; - ResponseAppend_P(PSTR("\"%s\":{\"MinInterval\":%d,\"MaxInterval\":%d"), attr_name, min_interval, max_interval); + ResponseAppend_P(PSTR("\"%s\":{\"MinInterval\":%d,\"MaxInterval\":%d"), attr_matched.name, min_interval, max_interval); buf.add8(0); // direction, always 0 buf.add16(attr_id); - buf.add8(attr_type); + buf.add8(attr_matched.zigbee_type); buf.add16(min_interval); buf.add16(max_interval); - if (!Z_isDiscreteDataType(attr_type)) { // report_change is only valid for non-discrete data types (numbers) - ZbApplyMultiplier(report_change, multiplier); + if (!Z_isDiscreteDataType(attr_matched.zigbee_type)) { // report_change is only valid for non-discrete data types (numbers) + ZbApplyMultiplier(report_change, attr_matched.multiplier); // encode value - int32_t res = encodeSingleAttribute(buf, report_change, "", attr_type); + int32_t res = encodeSingleAttribute(buf, report_change, "", attr_matched.zigbee_type); if (res < 0) { AddLog(LOG_LEVEL_ERROR, PSTR(D_LOG_ZIGBEE "internal error, unsupported attribute type")); } else { @@ -1690,12 +1689,12 @@ void Z_IncomingMessage(class ZCLFrame &zcl_received) { zcl_received.parseReadAttributesResponse(attr_list); if (clusterid) { defer_attributes = true; } // don't defer system Cluster=0 messages } else if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_READ_ATTRIBUTES == zcl_received.getCmdId())) { - zcl_received.parseReadAttributes(attr_list); + zcl_received.parseReadAttributes(srcaddr, attr_list); // never defer read_attributes, so the auto-responder can send response back on a per cluster basis } else if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_READ_REPORTING_CONFIGURATION_RESPONSE == zcl_received.getCmdId())) { - zcl_received.parseReadConfigAttributes(attr_list); + zcl_received.parseReadConfigAttributes(srcaddr, attr_list); } else if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_CONFIGURE_REPORTING_RESPONSE == zcl_received.getCmdId())) { - zcl_received.parseConfigAttributes(attr_list); + zcl_received.parseConfigAttributes(srcaddr, attr_list); } else if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_WRITE_ATTRIBUTES_RESPONSE == zcl_received.getCmdId())) { zcl_received.parseWriteAttributesResponse(attr_list); } else if (zcl_received.isClusterSpecificCommand()) { @@ -2123,6 +2122,15 @@ int32_t Z_Query_Bulbs(uint8_t value) { return 0; // continue } +// +// Z_ZbAutoload - autoload all definitions from filesystem +// files with ending '.zb' suffix +// +int32_t Z_ZbAutoload(uint8_t value) { + ZbAutoload(); + return 0; +} + // // Zigbee initialization is complete, let the party begin // @@ -2193,7 +2201,7 @@ void ZCLFrame::autoResponder(const uint16_t *attr_list_ids, size_t attr_len) { break; } if (!attr.isNone()) { - Z_parseAttributeKey(attr, cluster); + Z_parseAttributeKey(shortaddr, attr, cluster); attr_list.addAttribute(cluster, attr_id) = attr; } } diff --git a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_A_impl.ino b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_A_impl.ino index c7182d609..a5a3735be 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_A_impl.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_23_zigbee_A_impl.ino @@ -39,7 +39,8 @@ const char kZbCommands[] PROGMEM = D_PRFX_ZB "|" // prefix D_CMND_ZIGBEE_BIND "|" D_CMND_ZIGBEE_UNBIND "|" D_CMND_ZIGBEE_PING "|" D_CMND_ZIGBEE_MODELID "|" D_CMND_ZIGBEE_LIGHT "|" D_CMND_ZIGBEE_OCCUPANCY "|" D_CMND_ZIGBEE_RESTORE "|" D_CMND_ZIGBEE_BIND_STATE "|" D_CMND_ZIGBEE_MAP "|" D_CMND_ZIGBEE_LEAVE "|" - D_CMND_ZIGBEE_CONFIG "|" D_CMND_ZIGBEE_DATA "|" D_CMND_ZIGBEE_SCAN "|" D_CMND_ZIGBEE_ENROLL "|" D_CMND_ZIGBEE_CIE + D_CMND_ZIGBEE_CONFIG "|" D_CMND_ZIGBEE_DATA "|" D_CMND_ZIGBEE_SCAN "|" D_CMND_ZIGBEE_ENROLL "|" D_CMND_ZIGBEE_CIE "|" + D_CMND_ZIGBEE_LOAD "|" D_CMND_ZIGBEE_UNLOAD "|" D_CMND_ZIGBEE_LOADDUMP ; SO_SYNONYMS(kZbSynonyms, @@ -62,6 +63,7 @@ void (* const ZigbeeCommand[])(void) PROGMEM = { &CmndZbRestore, &CmndZbBindState, &CmndZbMap, &CmndZbLeave, &CmndZbConfig, &CmndZbData, &CmndZbScan, &CmndZbenroll, &CmndZbcie, + &CmndZbLoad, &CmndZbUnload, &CmndZbLoadDump, }; /********************************************************************************************/ @@ -339,7 +341,7 @@ void ZbSendReportWrite(class JsonParserToken val_pubwrite, class ZCLFrame & zcl) Z_attribute attr; attr.setKeyName(key.getStr()); - if (Z_parseAttributeKey(attr, tuya_protocol ? 0xEF00 : 0xFFFF)) { // favor tuya protocol if needed + if (Z_parseAttributeKey(zcl.shortaddr, attr, tuya_protocol ? 0xEF00 : 0xFFFF)) { // favor tuya protocol if needed // Buffer ready, do some sanity checks // all attributes must use the same cluster @@ -352,11 +354,11 @@ void ZbSendReportWrite(class JsonParserToken val_pubwrite, class ZCLFrame & zcl) } else { if (attr.key_is_str) { - Response_P(PSTR("{\"%s\":\"%s'%s'\"}"), XdrvMailbox.command, PSTR(D_ZIGBEE_UNKNOWN_ATTRIBUTE " "), key); + Response_P(PSTR("{\"%s\":\"%s'%s'\"}"), XdrvMailbox.command, PSTR(D_ZIGBEE_UNKNOWN_ATTRIBUTE " "), key.getStr()); return; } if (Zunk == attr.attr_type) { - Response_P(PSTR("{\"%s\":\"%s'%s'\"}"), XdrvMailbox.command, PSTR(D_ZIGBEE_UNSUPPORTED_ATTRIBUTE_TYPE " "), key); + Response_P(PSTR("{\"%s\":\"%s'%s'\"}"), XdrvMailbox.command, PSTR(D_ZIGBEE_UNSUPPORTED_ATTRIBUTE_TYPE " "), key.getStr()); return; } } @@ -623,34 +625,26 @@ void ZbSendRead(JsonParserToken val_attr, ZCLFrame & zcl) { bool found = false; // scan attributes to find by name, and retrieve type - for (uint32_t i = 0; i < nitems(Z_PostProcess); i++) { - const Z_AttributeConverter *converter = &Z_PostProcess[i]; - uint16_t local_attr_id = pgm_read_word(&converter->attribute); - uint16_t local_cluster_id = CxToCluster(pgm_read_byte(&converter->cluster_short)); - // uint8_t local_type_id = pgm_read_byte(&converter->type); - - if ((pgm_read_word(&converter->name_offset)) && (0 == strcasecmp_P(key.getStr(), Z_strings + pgm_read_word(&converter->name_offset)))) { - // match name - // check if there is a conflict with cluster - // TODO - if (!(value.getBool()) && attr_item_offset) { - // If value is false (non-default) then set direction to 1 (for ReadConfig) - attrs[actual_attr_len] = 0x01; - } - actual_attr_len += attr_item_offset; - attrs[actual_attr_len++] = local_attr_id & 0xFF; - attrs[actual_attr_len++] = local_attr_id >> 8; - actual_attr_len += attr_item_len - 2 - attr_item_offset; // normally 0 - found = true; - // check cluster - if (!zcl.validCluster()) { - zcl.cluster = local_cluster_id; - } else if (zcl.cluster != local_cluster_id) { - ResponseCmndChar_P(PSTR(D_ZIGBEE_TOO_MANY_CLUSTERS)); - if (attrs) { free(attrs); } - return; - } - break; // found, exit loop + Z_attribute_match matched_attr = Z_findAttributeMatcherByName(zcl.shortaddr, key.getStr()); + if (matched_attr.found()) { + // match name + // check if there is a conflict with cluster + if (!(value.getBool()) && attr_item_offset) { + // If value is false (non-default) then set direction to 1 (for ReadConfig) + attrs[actual_attr_len] = 0x01; + } + actual_attr_len += attr_item_offset; + attrs[actual_attr_len++] = matched_attr.attribute & 0xFF; + attrs[actual_attr_len++] = matched_attr.attribute >> 8; + actual_attr_len += attr_item_len - 2 - attr_item_offset; // normally 0 + found = true; + // check cluster + if (!zcl.validCluster()) { + zcl.cluster = matched_attr.cluster; + } else if (zcl.cluster != matched_attr.cluster) { + ResponseCmndChar_P(PSTR(D_ZIGBEE_TOO_MANY_CLUSTERS)); + if (attrs) { free(attrs); } + return; } } if (!found) { @@ -863,7 +857,10 @@ void ZbBindUnbind(bool unbind) { // false = bind, true = unbind if (val_cluster) { cluster = val_cluster.getUInt(cluster); // first convert as number if (0 == cluster) { - zigbeeFindAttributeByName(val_cluster.getStr(), &cluster, nullptr, nullptr); + Z_attribute_match attr_matched = Z_findAttributeMatcherByName(BAD_SHORTADDR, val_cluster.getStr()); + if (attr_matched.found()) { + cluster = attr_matched.cluster; + } } } @@ -1306,6 +1303,53 @@ void CmndZbSave(void) { ResponseCmndDone(); } +// +// Command `ZbLoad` +// Load a device specific zigbee template +// +void CmndZbLoad(void) { + // can be called before Zigbee is initialized + RemoveSpace(XdrvMailbox.data); + + bool ret = true;; + if (strcmp(XdrvMailbox.data, "*") == 0) { + ZbAutoload(); + } else { + ret = ZbLoad(XdrvMailbox.data); + } + if (ret) { + ResponseCmndDone(); + } else { + ResponseCmndError(); + } +} + +// +// Command `ZbUnload` +// Unload a template previously loaded +// +void CmndZbUnload(void) { + // can be called before Zigbee is initialized + RemoveSpace(XdrvMailbox.data); + + bool ret = ZbUnload(XdrvMailbox.data); + if (ret) { + ResponseCmndDone(); + } else { + ResponseCmndError(); + } +} + +// +// Command `ZbLoadDump` +// Load a device specific zigbee template +// +void CmndZbLoadDump(void) { + // can be called before Zigbee is initialized + ZbLoadDump(); + ResponseCmndDone(); +} + // // Command `ZbScan` // Run an energy scan diff --git a/tasmota/zigbee/giex_water.zb b/tasmota/zigbee/giex_water.zb new file mode 100644 index 000000000..fbc5f2fd9 --- /dev/null +++ b/tasmota/zigbee/giex_water.zb @@ -0,0 +1,14 @@ +#Z2Tv1 +# GiEX garden watering https://www.aliexpress.com/item/1005004222098040.html +:TS0601,_TZE200_sh1btabb +EF00/0101,WaterMode # duration=0 / capacity=1 +EF00/0102,WaterState # off=0 / on=1 +EF00/0365,IrrigationStartTime # (string) ex: "08:12:26" +EF00/0366,IrrigationTarget # (string) ex: "08:13:36" +EF00/0267,CycleIrrigationNumTimes # number of cycle irrigation times, set to 0 for single cycle +EF00/0268,IrrigationTarget # duration in minutes or capacity in Liters (depending on mode) +EF00/0269,CycleIrrigationInterval # cycle irrigation interval (minutes, max 1440) +EF00/026A,CurrentTemperature # (value ignored because isn't a valid tempurature reading. Misdocumented and usage unclear) +EF00/026C=0001/0021,2 # match to BatteryPercentage +EF00/026F,WaterConsumed # water consumed (Litres) +EF00/0372,LastIrrigationDuration # (string) Ex: "00:01:10,0"