diff --git a/lib/jsmn-shadinger-1.0/src/JsonGenerator.cpp b/lib/jsmn-shadinger-1.0/src/JsonGenerator.cpp index a9554a55a..0bdd77768 100644 --- a/lib/jsmn-shadinger-1.0/src/JsonGenerator.cpp +++ b/lib/jsmn-shadinger-1.0/src/JsonGenerator.cpp @@ -106,6 +106,13 @@ void JsonGeneratorObject::add(const char* key, const String & str) { post(); } +// Add up to 32 bits hex value +void JsonGeneratorObject::addHex32(const char* key, uint32_t uval32) { + char hex[16]; + snprintf_P(hex, sizeof(hex), PSTR("\"0x%8X\""), uval32); + addStrRaw(key, hex); +} + // Add a raw string, that will not be escaped. // Can be used to add a sub-array, sub-object or 'null', 'true', 'false' values void JsonGeneratorObject::addStrRaw(const char* key, const char * sval) { diff --git a/lib/jsmn-shadinger-1.0/src/JsonGenerator.h b/lib/jsmn-shadinger-1.0/src/JsonGenerator.h index 8670aabbd..589c60167 100644 --- a/lib/jsmn-shadinger-1.0/src/JsonGenerator.h +++ b/lib/jsmn-shadinger-1.0/src/JsonGenerator.h @@ -58,6 +58,7 @@ public: void add(const char* key, uint32_t uval32); void add(const char* key, int32_t uval32); void add(const char* key, const String & str); + void addHex32(const char* key, uint32_t uval32); void addStrRaw(const char* key, const char * sval); void addStr(const char* key, const char * sval); diff --git a/tasmota/CHANGELOG.md b/tasmota/CHANGELOG.md index 0205cc78d..ab6194513 100644 --- a/tasmota/CHANGELOG.md +++ b/tasmota/CHANGELOG.md @@ -24,6 +24,7 @@ - Add support for inverted NeoPixelBus data line by enabling ``#define USE_WS2812_INVERTED`` (#8988) - Add PWM dimmer color/trigger on tap, SO88 led, DGR WITH_LOCAL flag by Paul Diem (#9474) - Add support for stateful ACs using ``StateMode`` in tasmota-ir.bin by Arik Yavilevich (#9472) +- Add Zigbee ``ZbData`` command for better support of device specific data ## Released diff --git a/tasmota/i18n.h b/tasmota/i18n.h index a5978b9d2..6a79b950b 100644 --- a/tasmota/i18n.h +++ b/tasmota/i18n.h @@ -572,6 +572,7 @@ #define D_CMND_ZIGBEE_RESTORE "Restore" #define D_CMND_ZIGBEE_CONFIG "Config" #define D_JSON_ZIGBEE_CONFIG "Config" +#define D_CMND_ZIGBEE_DATA "Data" // Commands xdrv_25_A4988_Stepper.ino #define D_CMND_MOTOR "MOTOR" diff --git a/tasmota/support_light_list.ino b/tasmota/support_light_list.ino index 28d2af595..f439e18fa 100644 --- a/tasmota/support_light_list.ino +++ b/tasmota/support_light_list.ino @@ -72,6 +72,10 @@ public: T & addHead(void); T & addHead(const T &val); T & addToLast(void); + // add an element allocated externally + // memory is free by us now -- don't free it! + T & addHead(LList_elt * elt); + T & addToLast(LList_elt * elt); // iterator // see https://stackoverflow.com/questions/8164567/how-to-make-my-custom-type-to-work-with-range-based-for-loops @@ -174,6 +178,13 @@ T & LList::addHead(const T &val) { return elt->_val; } +template +T & LList::addHead(LList_elt * elt) { + elt->next(_head); // insert at the head + _head = elt; + return elt->_val; +} + template T & LList::addToLast(void) { LList_elt **curr_ptr = &_head; @@ -183,4 +194,15 @@ T & LList::addToLast(void) { LList_elt * elt = new LList_elt(); // create element *curr_ptr = elt; return elt->_val; -} \ No newline at end of file +} + +template +T & LList::addToLast(LList_elt * elt) { + LList_elt **curr_ptr = &_head; + while (*curr_ptr) { + curr_ptr = &((*curr_ptr)->_next); + } + *curr_ptr = elt; + elt->_next = nullptr; + return elt->_val; +} diff --git a/tasmota/xdrv_23_zigbee_2_devices.ino b/tasmota/xdrv_23_zigbee_2_devices.ino index 506962f3a..618954053 100644 --- a/tasmota/xdrv_23_zigbee_2_devices.ino +++ b/tasmota/xdrv_23_zigbee_2_devices.ino @@ -24,6 +24,361 @@ #endif const uint16_t kZigbeeSaveDelaySeconds = ZIGBEE_SAVE_DELAY_SECONDS; // wait for x seconds +enum class Z_Data_Type : uint8_t { + Z_Unknown = 0x00, + Z_Light = 1, // Lights 1-5 channels + Z_Plug = 2, // Plug power consumption + Z_PIR = 3, + Z_Alarm = 4, + Z_Thermo = 5, // Thermostat and sensor for home environment (temp, himudity, pressure) + Z_OnOff = 6, // OnOff, Buttons and Relays (always complements Lights and Plugs) + Z_Ext = 0xF, // extended for other values + Z_Device = 0xFF // special value when parsing Device level attributes +}; + +class Z_Data_Set; +/*********************************************************************************************\ + * Device specific data, sensors... +\*********************************************************************************************/ +class Z_Data { +public: + Z_Data(Z_Data_Type type = Z_Data_Type::Z_Unknown, uint8_t endpoint = 0) : _type(type), _endpoint(endpoint), _config(-1), _power(0) {} + inline Z_Data_Type getType(void) const { return _type; } + inline int8_t getConfig(void) const { return _config; } + inline void setConfig(int8_t config) { _config = config; } + + inline uint8_t getEndpoint(void) const { return _endpoint; } + + static const Z_Data_Type type = Z_Data_Type::Z_Unknown; + + friend class Z_Data_Set; +protected: + Z_Data_Type _type; // encoded on 4 bits, type of the device + uint8_t _endpoint; // source endpoint, or 0x00 if any endpoint + int8_t _config; // encoded on 4 bits, customize behavior + uint8_t _power; // power state if the type supports it +}; + +/*********************************************************************************************\ + * Device specific: On/Off, power up to 8 relays +\*********************************************************************************************/ + +// _config contains the number of valid OnOff relays: 0..7 +// _power contains a bitfield of relays values +class Z_Data_OnOff : public Z_Data { +public: + Z_Data_OnOff(uint8_t endpoint = 0) : + Z_Data(Z_Data_Type::Z_OnOff, endpoint) + { + _config = 1; // at least 1 OnOff + } + + + inline bool validPower(uint32_t relay = 0) const { return (_config > relay); } // power is declared + inline bool getPower(uint32_t relay = 0) const { return bitRead(_power, relay); } + void setPower(bool val, uint32_t relay = 0); + + static void toAttributes(Z_attribute_list & attr_list, const Z_Data_OnOff & light); + + static const Z_Data_Type type = Z_Data_Type::Z_OnOff; +}; + + +void Z_Data_OnOff::setPower(bool val, uint32_t relay) { + if (relay < 8) { + if (_config < relay) { _config = relay; } // we update the number of valid relays + bitWrite(_power, relay, val); + } +} + +/*********************************************************************************************\ + * Device specific: Light device +\*********************************************************************************************/ +class Z_Data_Plug : public Z_Data { +public: + Z_Data_Plug(uint8_t endpoint = 0) : + Z_Data(Z_Data_Type::Z_Plug, endpoint), + mains_voltage(0xFFFF), + mains_power(-0x8000) + {} + + inline bool validMainsVoltage(void) const { return 0xFFFF != mains_voltage; } + inline bool validMainsPower(void) const { return -0x8000 != mains_power; } + + inline uint16_t getMainsVoltage(void) const { return mains_voltage; } + inline int16_t getMainsPower(void) const { return mains_power; } + + inline void setMainsVoltage(uint16_t _mains_voltage) { mains_voltage = _mains_voltage; } + inline void setMainsPower(int16_t _mains_power) { mains_power = _mains_power; } + + static void toAttributes(Z_attribute_list & attr_list, const Z_Data_Plug & light); + + static const Z_Data_Type type = Z_Data_Type::Z_Plug; + // 4 bytes + uint16_t mains_voltage; // AC voltage + int16_t mains_power; // Active power +}; + +/*********************************************************************************************\ + * Device specific: Light device +\*********************************************************************************************/ +class Z_Data_Light : public Z_Data { +public: + Z_Data_Light(uint8_t endpoint = 0) : + Z_Data(Z_Data_Type::Z_Light, endpoint), + colormode(0xFF), + dimmer(0xFF), + sat(0xFF), + hue(0xFF), + ct(0xFFFF), + x(0xFFFF), + y(0xFFFF) + {} + + inline bool validColormode(void) const { return 0xFF != colormode; } + inline bool validDimmer(void) const { return 0xFF != dimmer; } + inline bool validSat(void) const { return 0xFF != sat; } + inline bool validHue(void) const { return 0xFFFF != hue; } + inline bool validCT(void) const { return 0xFFFF != ct; } + inline bool validX(void) const { return 0xFFFF != x; } + inline bool validY(void) const { return 0xFFFF != y; } + + inline uint8_t getColorMode(void) const { return colormode; } + inline uint8_t getDimmer(void) const { return dimmer; } + inline uint8_t getSat(void) const { return sat; } + inline uint16_t getHue(void) const { return changeUIntScale(hue, 0, 254, 0, 360); } + inline uint16_t getCT(void) const { return ct; } + inline uint16_t getX(void) const { return x; } + inline uint16_t getY(void) const { return y; } + + inline void setColorMode(uint8_t _colormode) { colormode = _colormode; } + inline void setDimmer(uint8_t _dimmer) { dimmer = _dimmer; } + inline void setSat(uint8_t _sat) { sat = _sat; } + inline void setHue(uint16_t _hue) { hue = changeUIntScale(_hue, 0, 360, 0, 254);; } + inline void setCT(uint16_t _ct) { ct = _ct; } + inline void setX(uint16_t _x) { x = _x; } + inline void setY(uint16_t _y) { y = _y; } + + static void toAttributes(Z_attribute_list & attr_list, const Z_Data_Light & light); + + static const Z_Data_Type type = Z_Data_Type::Z_Light; + // 12 bytes + uint8_t colormode; // 0x00: Hue/Sat, 0x01: XY, 0x02: CT | 0xFF not set, default 0x01 + uint8_t dimmer; // last Dimmer value: 0-254 | 0xFF not set, default 0x00 + uint8_t sat; // last Sat: 0..254 | 0xFF not set, default 0x00 + uint8_t hue; // last Hue: 0..359 | 0xFFFF not set, default 0 + uint16_t ct; // last CT: 153-500 | 0xFFFF not set, default 200 + uint16_t x, y; // last color [x,y] | 0xFFFF not set, default 0 +}; + +/*********************************************************************************************\ + * Device specific: Sensors: temp, humidity, pressure... +\*********************************************************************************************/ +class Z_Data_Thermo : public Z_Data { +public: + Z_Data_Thermo(uint8_t endpoint = 0) : + Z_Data(Z_Data_Type::Z_Thermo, endpoint), + temperature(-0x8000), + pressure(0xFFFF), + humidity(0xFFFF), + th_setpoint(0xFF), + temperature_target(-0x8000) + {} + + inline bool validTemperature(void) const { return -0x8000 != temperature; } + inline bool validPressure(void) const { return 0xFFFF != pressure; } + inline bool validHumidity(void) const { return 0xFFFF != humidity; } + inline bool validThSetpoint(void) const { return 0xFF != th_setpoint; } + inline bool validTempTarget(void) const { return -0x8000 != temperature_target; } + + inline int16_t getTemperature(void) const { return temperature; } + inline uint16_t getPressure(void) const { return pressure; } + inline uint16_t getHumidity(void) const { return humidity; } + inline uint8_t getThSetpoint(void) const { return th_setpoint; } + inline int16_t getTempTarget(void) const { return temperature_target; } + + inline void setTemperature(int16_t _temperature) { temperature = _temperature; } + inline void setPressure(uint16_t _pressure) { pressure = _pressure; } + inline void setHumidity(uint16_t _humidity) { humidity = _humidity; } + inline void setThSetpoint(uint8_t _th_setpoint) { th_setpoint = _th_setpoint; } + inline void setTempTarget(int16_t _temperature_target){ temperature_target = _temperature_target; } + + static void toAttributes(Z_attribute_list & attr_list, const Z_Data_Thermo & thermo); + + static const Z_Data_Type type = Z_Data_Type::Z_Thermo; + // 8 bytes + // sensor data + int16_t temperature; // temperature in 1/10th of Celsius, 0x8000 if unknown + uint16_t pressure; // air pressure in hPa, 0xFFFF if unknown + uint16_t humidity; // humidity in percent, 0..100, 0xFF if unknown + // thermostat + uint8_t th_setpoint; // percentage of heat/cool in percent + int16_t temperature_target; // settings for the temparature +}; + +/*********************************************************************************************\ + * Device specific: Alarm +\*********************************************************************************************/ +class Z_Data_Alarm : public Z_Data { +public: + Z_Data_Alarm(uint8_t endpoint = 0) : + Z_Data(Z_Data_Type::Z_Alarm, endpoint), + zone_type(0xFFFF) + {} + + static const Z_Data_Type type = Z_Data_Type::Z_Alarm; + + inline bool validZoneType(void) const { return 0xFFFF != zone_type; } + + inline uint16_t getZoneType(void) const { return zone_type; } + + inline void setZoneType(uint16_t _zone_type) { zone_type = _zone_type; } + + static void toAttributes(Z_attribute_list & attr_list, const Z_Data_Alarm & alarm); + + // 4 bytes + uint16_t zone_type; // mapped to the Zigbee standard + // 0x0000 Standard CIE + // 0x000d Motion sensor + // 0x0015 Contact switch + // 0x0028 Fire sensor + // 0x002a Water sensor + // 0x002b Carbon Monoxide (CO) sensor + // 0x002c Personal emergency device + // 0x002d Vibration/Movement sensor + // 0x010f Remote Control + // 0x0115 Key fob + // 0x021d Keypad + // 0x0225 Standard Warning Device (see [N1] part 4) + // 0x0226 Glass break sensor + // 0x0229 Security repeater* +}; +/*********************************************************************************************\ + * + * Device specific Linked List + * +\*********************************************************************************************/ +class Z_Data_Set : public LList { +public: + // List() : List() {} + Z_Data & getByType(Z_Data_Type type, uint8_t ep = 0); // creates if non-existent + const Z_Data & find(Z_Data_Type type, uint8_t ep = 0) const; + + // getX() always returns a valid object, and creates the object if there is none + // find() does not create an object if it does not exist, and returns *(X*)nullptr + + + template + M & get(uint8_t ep = 0); + + template + const M & find(uint8_t ep = 0) const; + + // check if the point is null, if so create a new object with the right sub-class + template + M & addIfNull(M & cur, uint8_t ep = 0); +}; + +Z_Data & Z_Data_Set::getByType(Z_Data_Type type, uint8_t ep) { + switch (type) { + case Z_Data_Type::Z_Light: + return get(ep); + case Z_Data_Type::Z_Plug: + return get(ep); + case Z_Data_Type::Z_Alarm: + return get(ep); + case Z_Data_Type::Z_Thermo: + return get(ep); + case Z_Data_Type::Z_OnOff: + return get(ep); + default: + return *(Z_Data*)nullptr; + } +} + +template +M & Z_Data_Set::get(uint8_t ep) { + M & m = (M&) find(M::type, ep); + return addIfNull(m, ep); +} + +template +const M & Z_Data_Set::find(uint8_t ep) const { + return (M&) find(M::type, ep); +} + +// Input: reference to object +// Output: if reference is null, instantiate object and add it at the end of the list +template +M & Z_Data_Set::addIfNull(M & cur, uint8_t ep) { + if (nullptr == &cur) { + LList_elt * elt = new LList_elt(); + elt->val()._endpoint = ep; + this->addToLast((LList_elt*)elt); + return elt->val(); + } else { + if (cur._endpoint == 0) { cur._endpoint = ep; } // be more specific on endpoint + return cur; + } +} + +const Z_Data & Z_Data_Set::find(Z_Data_Type type, uint8_t ep) const { + for (auto & elt : *this) { + if (elt._type == type) { + // type matches, check if ep matches. + // 0 matches all endpoints + if ((ep == 0) || (elt._endpoint == 0) || (ep == elt._endpoint)) { + return elt; + } + } + } + return *(Z_Data*)nullptr; +} + + +// Low-level +// Add light attributes, used by dumpLightState and by SbData +// +void Z_Data_Plug::toAttributes(Z_attribute_list & attr_list, const Z_Data_Plug & plug) { + if (&plug == nullptr) { return; } + // dump all known values + if (plug.validMainsVoltage()) { attr_list.addAttribute(PSTR("RMSVoltage")).setUInt(plug.getMainsVoltage()); } + if (plug.validMainsPower()) { attr_list.addAttribute(PSTR("ActivePower")).setInt(plug.getMainsPower()); } +} + +void Z_Data_Light::toAttributes(Z_attribute_list & attr_list, const Z_Data_Light & light) { + if (&light == nullptr) { return; } + // expose the last known status of the bulb, for Hue integration + attr_list.addAttribute(PSTR(D_JSON_ZIGBEE_LIGHT)).setInt(light.getConfig()); // special case, since type is 0x00 we can assume getConfig() is good + // dump all known values + if (light.validDimmer()) { attr_list.addAttribute(PSTR("Dimmer")).setUInt(light.getDimmer()); } + if (light.validColormode()) { attr_list.addAttribute(PSTR("Colormode")).setUInt(light.getColorMode()); } + if (light.validCT()) { attr_list.addAttribute(PSTR("CT")).setUInt(light.getCT()); } + if (light.validSat()) { attr_list.addAttribute(PSTR("Sat")).setUInt(light.getSat()); } + if (light.validHue()) { attr_list.addAttribute(PSTR("Hue")).setUInt(light.getHue()); } + if (light.validX()) { attr_list.addAttribute(PSTR("X")).setUInt(light.getX()); } + if (light.validY()) { attr_list.addAttribute(PSTR("Y")).setUInt(light.getY()); } +} + +void Z_Data_OnOff::toAttributes(Z_attribute_list & attr_list, const Z_Data_OnOff & onoff) { + if (&onoff == nullptr) { return; } + if (onoff.validPower()) { attr_list.addAttribute(PSTR("Power")).setUInt(onoff.getPower() ? 1 : 0); } +} + +void Z_Data_Thermo::toAttributes(Z_attribute_list & attr_list, const Z_Data_Thermo & thermo) { + if (&thermo == nullptr) { return; } + if (thermo.validTemperature()) { attr_list.addAttribute(PSTR("Temperature")).setInt(thermo.getTemperature()); } + if (thermo.validPressure()) { attr_list.addAttribute(PSTR("Pressure")).setUInt(thermo.getPressure()); } + if (thermo.validHumidity()) { attr_list.addAttribute(PSTR("Humidity")).setUInt(thermo.getHumidity()); } + if (thermo.validThSetpoint()) { attr_list.addAttribute(PSTR("ThSetpoint")).setUInt(thermo.getThSetpoint()); } + if (thermo.validTempTarget()) { attr_list.addAttribute(PSTR("TempTarget")).setInt(thermo.getTempTarget()); } +} + +void Z_Data_Alarm::toAttributes(Z_attribute_list & attr_list, const Z_Data_Alarm & alarm) { + if (&alarm == nullptr) { return; } + if (alarm.validZoneType()) { attr_list.addAttribute(PSTR("ZoneType")).setUInt(alarm.getZoneType()); } +} + /*********************************************************************************************\ * Structures for Rules variables related to the last received message \*********************************************************************************************/ @@ -46,39 +401,17 @@ public: // sequence number for Zigbee frames uint16_t shortaddr; // unique key if not null, or unspecified if null uint8_t seqNumber; + bool hidden; + bool reachable; // Light information for Hue integration integration, last known values - uint8_t zb_profile; // profile of the device - // high 4 bits is device type: - // 0x0. = bulb - // 0x1. = switch - // 0x2. = motion sensor - // 0x3. = other alarms - // 0xE. = reserved for extension - // 0xF. = unknown - // For Bulb (0x0.) - // 0x0N = number of channel for the bulb: 0-5 - // 0x08 = the device is hidden from Alexa + + // New version of device data handling + Z_Data_Set data; // Linkedlist of device data per endpoint // other status - uint8_t power; // power state (boolean), MSB (0x80) stands for reachable - uint8_t colormode; // 0x00: Hue/Sat, 0x01: XY, 0x02: CT | 0xFF not set, default 0x01 - uint8_t dimmer; // last Dimmer value: 0-254 | 0xFF not set, default 0x00 - uint8_t sat; // last Sat: 0..254 | 0xFF not set, default 0x00 - uint16_t ct; // last CT: 153-500 | 0xFFFF not set, default 200 - uint16_t hue; // last Hue: 0..359 | 0xFFFF not set, default 0 - uint16_t x, y; // last color [x,y] | 0xFFFF not set, default 0 uint8_t lqi; // lqi from last message, 0xFF means unknown uint8_t batterypercent; // battery percentage (0..100), 0xFF means unknwon - // sensor data - int16_t temperature; // temperature in 1/10th of Celsius, 0x8000 if unknown - uint16_t pressure; // air pressure in hPa, 0xFFFF if unknown - uint8_t humidity; // humidity in percent, 0..100, 0xFF if unknown - // power plug data - uint16_t mains_voltage; // AC voltage - int16_t mains_power; // Active power + // power plug data- uint32_t last_seen; // Last seen time (epoch) - // thermostat - int16_t temperature_target; // settings for the temparature - uint8_t th_setpoint; // percentage of heat/cool in percent // Constructor with all defaults Z_Device(uint16_t _shortaddr = BAD_SHORTADDR, uint64_t _longaddr = 0x00): @@ -91,26 +424,12 @@ public: attr_list(), shortaddr(_shortaddr), seqNumber(0), + hidden(false), + reachable(false), // Hue support - zb_profile(0xFF), // no profile - power(0x02), // 0x80 = reachable, 0x01 = power on, 0x02 = power unknown - colormode(0xFF), - dimmer(0xFF), - sat(0xFF), - ct(0xFFFF), - hue(0xFFFF), - x(0xFFFF), - y(0xFFFF), lqi(0xFF), batterypercent(0xFF), - temperature(-0x8000), - pressure(0xFFFF), - humidity(0xFF), - mains_voltage(0xFFFF), - mains_power(-0x8000), - last_seen(0), - temperature_target(-0x8000), - th_setpoint(0xFF) + last_seen(0) { }; inline bool valid(void) const { return BAD_SHORTADDR != shortaddr; } // is the device known, valid and found? @@ -120,40 +439,53 @@ public: inline bool validModelId(void) const { return nullptr != modelId; } inline bool validFriendlyName(void) const { return nullptr != friendlyName; } - inline bool validPower(void) const { return 0x00 == (power & 0x02); } - inline bool validColormode(void) const { return 0xFF != colormode; } - inline bool validDimmer(void) const { return 0xFF != dimmer; } - inline bool validSat(void) const { return 0xFF != sat; } - inline bool validCT(void) const { return 0xFFFF != ct; } - inline bool validHue(void) const { return 0xFFFF != hue; } - inline bool validX(void) const { return 0xFFFF != x; } - inline bool validY(void) const { return 0xFFFF != y; } + inline bool validPower(uint8_t ep =0) const; inline bool validLqi(void) const { return 0xFF != lqi; } inline bool validBatteryPercent(void) const { return 0xFF != batterypercent; } - - inline bool validTemperature(void) const { return -0x8000 != temperature; } - inline bool validPressure(void) const { return 0xFFFF != pressure; } - inline bool validHumidity(void) const { return 0xFF != humidity; } inline bool validLastSeen(void) const { return 0x0 != last_seen; } - inline bool validTemperatureTarget(void) const { return -0x8000 != temperature_target; } - inline bool validThSetpoint(void) const { return 0xFF != th_setpoint; } + inline void setReachable(bool _reachable) { reachable = _reachable; } + inline bool getReachable(void) const { return reachable; } + inline bool getPower(uint8_t ep =0) const; - inline bool validMainsVoltage(void) const { return 0xFFFF != mains_voltage; } - inline bool validMainsPower(void) const { return -0x8000 != mains_power; } + // dump device attributes to ZbData + void toAttributes(Z_attribute_list & attr_list) const; - inline void setReachable(bool reachable) { bitWrite(power, 7, reachable); } - inline bool getReachable(void) const { return bitRead(power, 7); } - inline void setPower(bool power_on) { bitWrite(power, 0, power_on); bitWrite(power, 1, false); } - inline bool getPower(void) const { return bitRead(power, 0); } + // Device data specific + void setPower(bool power_on, uint8_t ep = 0); // If light, returns the number of channels, or 0xFF if unknown - uint8_t getLightChannels(void) const { - if ((zb_profile & 0xF0) == 0x00) { - return zb_profile & 0x07; + int8_t getLightChannels(void) const { + const Z_Data_Light & light = data.find(0); + if (&light != nullptr) { + return light.getConfig(); + } else { + return -1; } - return 0xFF; + } + + // returns: dirty flag, did we change the value of the object + bool setLightChannels(int8_t channels) { + bool dirty = false; + if (channels >= 0) { + // retrieve of create light object + Z_Data_Light & light = data.get(0); + if (channels != light.getConfig()) { + light.setConfig(channels); + dirty = true; + } + } else { + // remove light object if any + for (auto & data_elt : data) { + if (data_elt.getType() == Z_Data_Type::Z_Light) { + // remove light object + data.remove(&data_elt); + dirty = true; + } + } + } + return dirty; } }; @@ -268,19 +600,20 @@ public: uint8_t getNextSeqNumber(uint16_t shortaddr); // Dump json + static void addLightState(Z_attribute_list & attr_list, const Z_Data_Light & light); String dumpLightState(uint16_t shortaddr) const; String dump(uint32_t dump_mode, uint16_t status_shortaddr = 0) const; int32_t deviceRestore(JsonParserObject json); // General Zigbee device profile support - void setZbProfile(uint16_t shortaddr, uint8_t zb_profile); - uint8_t getZbProfile(uint16_t shortaddr) const ; + void setLightProfile(uint16_t shortaddr, uint8_t light_profile); + uint8_t getLightProfile(uint16_t shortaddr) const ; // Hue support - void setHueBulbtype(uint16_t shortaddr, int8_t bulbtype); int8_t getHueBulbtype(uint16_t shortaddr) const ; void hideHueBulb(uint16_t shortaddr, bool hidden); bool isHueBulbHidden(uint16_t shortaddr) const ; + Z_Data_Light & getLight(uint16_t shortaddr); // Timers void resetTimersForDevice(uint16_t shortaddr, uint16_t groupaddr, uint8_t category, uint16_t cluster = 0xFFFF, uint8_t endpoint = 0xFF); @@ -334,8 +667,6 @@ private: void freeDeviceEntry(Z_Device *device); void setStringAttribute(char*& attr, const char * str); - - void updateZbProfile(uint16_t shortaddr); }; /*********************************************************************************************\ @@ -347,787 +678,6 @@ Z_Devices zigbee_devices = Z_Devices(); uint64_t localIEEEAddr = 0; uint16_t localShortAddr = 0; -/*********************************************************************************************\ - * Implementation -\*********************************************************************************************/ -// -// Create a new Z_Device entry in _devices. Only to be called if you are sure that no -// entry with same shortaddr or longaddr exists. -// -Z_Device & Z_Devices::createDeviceEntry(uint16_t shortaddr, uint64_t longaddr) { - if ((BAD_SHORTADDR == shortaddr) && !longaddr) { return (Z_Device&) device_unk; } // it is not legal to create this entry - Z_Device device(shortaddr, longaddr); - - dirty(); - return _devices.addHead(device); -} - -void Z_Devices::freeDeviceEntry(Z_Device *device) { - if (device->manufacturerId) { free(device->manufacturerId); } - if (device->modelId) { free(device->modelId); } - if (device->friendlyName) { free(device->friendlyName); } - free(device); -} - -// -// Scan all devices to find a corresponding shortaddr -// Looks info device.shortaddr entry -// In: -// shortaddr (not BAD_SHORTADDR) -// Out: -// reference to device, or to device_unk if not found -// (use foundDevice() to check if found) -Z_Device & Z_Devices::findShortAddr(uint16_t shortaddr) { - for (auto & elem : _devices) { - if (elem.shortaddr == shortaddr) { return elem; } - } - return (Z_Device&) device_unk; -} -const Z_Device & Z_Devices::findShortAddr(uint16_t shortaddr) const { - for (const auto & elem : _devices) { - if (elem.shortaddr == shortaddr) { return elem; } - } - return device_unk; -} -// -// Scan all devices to find a corresponding longaddr -// Looks info device.longaddr entry -// In: -// longaddr (non null) -// Out: -// index in _devices of entry, -1 if not found -// -Z_Device & Z_Devices::findLongAddr(uint64_t longaddr) { - if (!longaddr) { return (Z_Device&) device_unk; } - for (auto &elem : _devices) { - if (elem.longaddr == longaddr) { return elem; } - } - return (Z_Device&) device_unk; -} -const Z_Device & Z_Devices::findLongAddr(uint64_t longaddr) const { - if (!longaddr) { return device_unk; } - for (const auto &elem : _devices) { - if (elem.longaddr == longaddr) { return elem; } - } - return device_unk; -} -// -// Scan all devices to find a corresponding friendlyNme -// Looks info device.friendlyName entry -// In: -// friendlyName (null terminated, should not be empty) -// Out: -// index in _devices of entry, -1 if not found -// -int32_t Z_Devices::findFriendlyName(const char * name) const { - if (!name) { return -1; } // if pointer is null - size_t name_len = strlen(name); - int32_t found = 0; - if (name_len) { - for (auto &elem : _devices) { - if (elem.friendlyName) { - if (strcasecmp(elem.friendlyName, name) == 0) { return found; } - } - found++; - } - } - return -1; -} - -uint16_t Z_Devices::isKnownLongAddr(uint64_t longaddr) const { - const Z_Device & device = findLongAddr(longaddr); - if (foundDevice(device)) { - return device.shortaddr; // can be zero, if not yet registered - } else { - return BAD_SHORTADDR; - } -} - -uint16_t Z_Devices::isKnownIndex(uint32_t index) const { - if (index < devicesSize()) { - const Z_Device & device = devicesAt(index); - return device.shortaddr; - } else { - return BAD_SHORTADDR; - } -} - -uint16_t Z_Devices::isKnownFriendlyName(const char * name) const { - if ((!name) || (0 == strlen(name))) { return BAD_SHORTADDR; } // Error - int32_t found = findFriendlyName(name); - if (found >= 0) { - const Z_Device & device = devicesAt(found); - return device.shortaddr; // can be zero, if not yet registered - } else { - return BAD_SHORTADDR; - } -} - -uint64_t Z_Devices::getDeviceLongAddr(uint16_t shortaddr) const { - return findShortAddr(shortaddr).longaddr; // if unknown, it reverts to the Unknown device and longaddr is 0x00 -} - -// -// We have a seen a shortaddr on the network, get the corresponding device object -// -Z_Device & Z_Devices::getShortAddr(uint16_t shortaddr) { - if (BAD_SHORTADDR == shortaddr) { return (Z_Device&) device_unk; } // this is not legal - Z_Device & device = findShortAddr(shortaddr); - if (foundDevice(device)) { - return device; - } - return createDeviceEntry(shortaddr, 0); -} - -// find the Device object by its longaddr (unique key if not null) -Z_Device & Z_Devices::getLongAddr(uint64_t longaddr) { - if (!longaddr) { return (Z_Device&) device_unk; } - Z_Device & device = findLongAddr(longaddr); - if (foundDevice(device)) { - return device; - } - return createDeviceEntry(0, longaddr); -} - -// Remove device from list, return true if it was known, false if it was not recorded -bool Z_Devices::removeDevice(uint16_t shortaddr) { - Z_Device & device = findShortAddr(shortaddr); - if (foundDevice(device)) { - _devices.remove(&device); - dirty(); - return true; - } - return false; -} - -// -// We have just seen a device on the network, update the info based on short/long addr -// In: -// shortaddr -// longaddr (both can't be null at the same time) -void Z_Devices::updateDevice(uint16_t shortaddr, uint64_t longaddr) { - Z_Device * s_found = &findShortAddr(shortaddr); // is there already a shortaddr entry - Z_Device * l_found = &findLongAddr(longaddr); // is there already a longaddr entry - - if (foundDevice(*s_found) && foundDevice(*l_found)) { // both shortaddr and longaddr are already registered - if (s_found == l_found) { - } else { // they don't match - // the device with longaddr got a new shortaddr - l_found->shortaddr = shortaddr; // update the shortaddr corresponding to the longaddr - // erase the previous shortaddr - freeDeviceEntry(s_found); - _devices.remove(s_found); - dirty(); - } - } else if (foundDevice(*s_found)) { - // shortaddr already exists but longaddr not - // add the longaddr to the entry - s_found->longaddr = longaddr; - dirty(); - } else if (foundDevice(*l_found)) { - // longaddr entry exists, update shortaddr - l_found->shortaddr = shortaddr; - dirty(); - } else { - // neither short/lonf addr are found. - if ((BAD_SHORTADDR != shortaddr) || longaddr) { - createDeviceEntry(shortaddr, longaddr); - } - } -} - -// -// Clear all endpoints -// -void Z_Devices::clearEndpoints(uint16_t shortaddr) { - Z_Device &device = getShortAddr(shortaddr); - for (uint32_t i = 0; i < endpoints_max; i++) { - device.endpoints[i] = 0; - // no dirty here because it doesn't make sense to store it, does it? - } -} - -// -// Add an endpoint to a shortaddr -// -void Z_Devices::addEndpoint(uint16_t shortaddr, uint8_t endpoint) { - if (0x00 == endpoint) { return; } - Z_Device &device = getShortAddr(shortaddr); - - for (uint32_t i = 0; i < endpoints_max; i++) { - if (endpoint == device.endpoints[i]) { - return; // endpoint already there - } - if (0 == device.endpoints[i]) { - device.endpoints[i] = endpoint; - dirty(); - return; - } - } -} - -// -// Count the number of known endpoints -// -uint32_t Z_Devices::countEndpoints(uint16_t shortaddr) const { - uint32_t count_ep = 0; - const Z_Device & device =findShortAddr(shortaddr); - if (!foundDevice(device)) return 0; - - for (uint32_t i = 0; i < endpoints_max; i++) { - if (0 != device.endpoints[i]) { - count_ep++; - } - } - return count_ep; -} - -// Find the first endpoint of the device -uint8_t Z_Devices::findFirstEndpoint(uint16_t shortaddr) const { - // When in router of end-device mode, the coordinator was not probed, in this case always talk to endpoint 1 - if (0x0000 == shortaddr) { return 1; } - return findShortAddr(shortaddr).endpoints[0]; // returns 0x00 if no endpoint -} - -void Z_Devices::setStringAttribute(char*& attr, const char * str) { - if (nullptr == str) { return; } // ignore a null parameter - size_t str_len = strlen(str); - - if ((nullptr == attr) && (0 == str_len)) { return; } // if both empty, don't do anything - if (attr) { - // we already have a value - if (strcmp(attr, str) != 0) { - // new value - free(attr); // free previous value - attr = nullptr; - } else { - return; // same value, don't change anything - } - } - if (str_len) { - attr = (char*) malloc(str_len + 1); - strlcpy(attr, str, str_len + 1); - } - dirty(); -} - -// -// Sets the ManufId for a device. -// No action taken if the device does not exist. -// Inputs: -// - shortaddr: 16-bits short address of the device. No action taken if the device is unknown -// - str: string pointer, if nullptr it is considered as empty string -// Impact: -// - Any actual change in ManufId (i.e. setting a different value) triggers a `dirty()` and saving to Flash -// -void Z_Devices::setManufId(uint16_t shortaddr, const char * str) { - setStringAttribute(getShortAddr(shortaddr).manufacturerId, str); -} - -void Z_Devices::setModelId(uint16_t shortaddr, const char * str) { - setStringAttribute(getShortAddr(shortaddr).modelId, str); -} - -void Z_Devices::setFriendlyName(uint16_t shortaddr, const char * str) { - setStringAttribute(getShortAddr(shortaddr).friendlyName, str); -} - - -void Z_Devices::setReachable(uint16_t shortaddr, bool reachable) { - getShortAddr(shortaddr).setReachable(reachable); -} - -void Z_Devices::setLQI(uint16_t shortaddr, uint8_t lqi) { - if (shortaddr == localShortAddr) { return; } - getShortAddr(shortaddr).lqi = lqi; -} - -void Z_Devices::setLastSeenNow(uint16_t shortaddr) { - if (shortaddr == localShortAddr) { return; } - // Only update time if after 2020-01-01 0000. - // Fixes issue where zigbee device pings before WiFi/NTP has set utc_time - // to the correct time, and "last seen" calculations are based on the - // pre-corrected last_seen time and the since-corrected utc_time. - if (Rtc.utc_time < 1577836800) { return; } - getShortAddr(shortaddr).last_seen = Rtc.utc_time; -} - - -void Z_Devices::setBatteryPercent(uint16_t shortaddr, uint8_t bp) { - getShortAddr(shortaddr).batterypercent = bp; -} - -// get the next sequance number for the device, or use the global seq number if device is unknown -uint8_t Z_Devices::getNextSeqNumber(uint16_t shortaddr) { - Z_Device & device = findShortAddr(shortaddr); - if (foundDevice(device)) { - device.seqNumber += 1; - return device.seqNumber; - } else { - _seqNumber += 1; - return _seqNumber; - } -} - -// General Zigbee device profile support -void Z_Devices::setZbProfile(uint16_t shortaddr, uint8_t zb_profile) { - Z_Device &device = getShortAddr(shortaddr); - if (zb_profile != device.zb_profile) { - device.zb_profile = zb_profile; - updateZbProfile(shortaddr); - dirty(); - } -} - -// Do all the required action when a profile is changed -void Z_Devices::updateZbProfile(uint16_t shortaddr) { - Z_Device &device = getShortAddr(shortaddr); - uint8_t zb_profile = device.zb_profile; - if (0xFF == zb_profile) { return; } - - switch (zb_profile & 0xF0) { - case 0x00: // bulb profile - { - uint32_t channels = zb_profile & 0x07; - // depending on the bulb type, the default parameters from unknown to credible defaults - // if (!device.validPower()) { device.setPower(false); } - // if (1 <= channels) { - // if (0xFF == device.dimmer) { device.dimmer = 0; } - // } - // if (3 <= channels) { - // if (0xFF == device.sat) { device.sat = 0; } - // if (0xFFFF == device.hue) { device.hue = 0; } - // if (0xFFFF == device.x) { device.x = 0; } - // if (0xFFFF == device.y) { device.y = 0; } - // if (0xFF == device.colormode) { device.colormode = 0; } // HueSat mode - // } - // if ((2 == channels) || (5 == channels)) { - // if (0xFFFF == device.ct) { device.ct = 200; } - // if (0xFF == device.colormode) { device.colormode = 2; } // CT mode - // } - } - break; - } -} - -// Returns the device profile or 0xFF if the device or profile is unknown -uint8_t Z_Devices::getZbProfile(uint16_t shortaddr) const { - return findShortAddr(shortaddr).zb_profile; -} - -// Hue support -void Z_Devices::setHueBulbtype(uint16_t shortaddr, int8_t bulbtype) { - uint8_t zb_profile = (0 > bulbtype) ? 0xFF : (bulbtype & 0x07); - setZbProfile(shortaddr, zb_profile); -} - -int8_t Z_Devices::getHueBulbtype(uint16_t shortaddr) const { - uint8_t zb_profile = getZbProfile(shortaddr); - if (0x00 == (zb_profile & 0xF0)) { - return (zb_profile & 0x07); - } else { - // not a bulb - return -1; - } -} - -void Z_Devices::hideHueBulb(uint16_t shortaddr, bool hidden) { - uint8_t hue_hidden_flag = hidden ? 0x08 : 0x00; - - Z_Device &device = getShortAddr(shortaddr); - if (0x00 == (device.zb_profile & 0xF0)) { - // bulb type - // set bit 3 accordingly - if (hue_hidden_flag != (device.zb_profile & 0x08)) { - device.zb_profile = (device.zb_profile & 0xF7) | hue_hidden_flag; - dirty(); - } - } -} -// true if device is not knwon or not a bulb - it wouldn't make sense to publish a non-bulb -bool Z_Devices::isHueBulbHidden(uint16_t shortaddr) const { - const Z_Device & device = findShortAddr(shortaddr); - if (foundDevice(device)) { - uint8_t zb_profile = device.zb_profile; - if (0x00 == (zb_profile & 0xF0)) { - // bulb type - return (zb_profile & 0x08) ? true : false; - } - } - return true; // Fallback - Device is considered as hidden -} - -// Deferred actions -// Parse for a specific category, of all deferred for a device if category == 0xFF -// Only with specific cluster number or for all clusters if cluster == 0xFFFF -void Z_Devices::resetTimersForDevice(uint16_t shortaddr, uint16_t groupaddr, uint8_t category, uint16_t cluster, uint8_t endpoint) { - // iterate the list of deferred, and remove any linked to the shortaddr - for (auto & defer : _deferred) { - if ((defer.shortaddr == shortaddr) && (defer.groupaddr == groupaddr)) { - if ((0xFF == category) || (defer.category == category)) { - if ((0xFFFF == cluster) || (defer.cluster == cluster)) { - if ((0xFF == endpoint) || (defer.endpoint == endpoint)) { - _deferred.remove(&defer); - } - } - } - } - } -} - -// Set timer for a specific device -void Z_Devices::setTimer(uint16_t shortaddr, uint16_t groupaddr, uint32_t wait_ms, uint16_t cluster, uint8_t endpoint, uint8_t category, uint32_t value, Z_DeviceTimer func) { - // First we remove any existing timer for same device in same category, except for category=0x00 (they need to happen anyway) - if (category >= Z_CLEAR_DEVICE) { // if category == 0, we leave all previous timers - resetTimersForDevice(shortaddr, groupaddr, category, category >= Z_CLEAR_DEVICE_CLUSTER ? cluster : 0xFFFF, category >= Z_CLEAR_DEVICE_CLUSTER_ENDPOINT ? endpoint : 0xFF); // remove any cluster - } - - // Now create the new timer - Z_Deferred & deferred = _deferred.addHead(); - deferred = { wait_ms + millis(), // timer - shortaddr, - groupaddr, - cluster, - endpoint, - category, - value, - func }; -} - -// Set timer after the already queued events -// I.e. the wait_ms is not counted from now, but from the last event queued, which is 'now' or in the future -void Z_Devices::queueTimer(uint16_t shortaddr, uint16_t groupaddr, uint32_t wait_ms, uint16_t cluster, uint8_t endpoint, uint8_t category, uint32_t value, Z_DeviceTimer func) { - Z_Device & device = getShortAddr(shortaddr); - uint32_t now_millis = millis(); - if (TimeReached(device.defer_last_message_sent)) { - device.defer_last_message_sent = now_millis; - } - // defer_last_message_sent equals now or a value in the future - device.defer_last_message_sent += wait_ms; - - // for queueing we don't clear the backlog, so we force category to Z_CAT_ALWAYS - setTimer(shortaddr, groupaddr, (device.defer_last_message_sent - now_millis), cluster, endpoint, Z_CAT_ALWAYS, value, func); -} - -// Run timer at each tick -// WARNING: don't set a new timer within a running timer, this causes memory corruption -void Z_Devices::runTimer(void) { - // visit all timers - for (auto & defer : _deferred) { - uint32_t timer = defer.timer; - if (TimeReached(timer)) { - (*defer.func)(defer.shortaddr, defer.groupaddr, defer.cluster, defer.endpoint, defer.value); - _deferred.remove(&defer); - } - } - - // check if we need to save to Flash - if ((_saveTimer) && TimeReached(_saveTimer)) { - saveZigbeeDevices(); - _saveTimer = 0; - } -} - -// does the new payload conflicts with the existing payload, i.e. values would be overwritten -// true - one attribute (except LinkQuality) woudl be lost, there is conflict -// false - new attributes can be safely added -bool Z_Devices::jsonIsConflict(uint16_t shortaddr, const Z_attribute_list &attr_list) const { - const Z_Device & device = findShortAddr(shortaddr); - - if (!foundDevice(device)) { return false; } - if (attr_list.isEmpty()) { - return false; // if no previous value, no conflict - } - - // compare groups - if (device.attr_list.isValidGroupId() && attr_list.isValidGroupId()) { - if (device.attr_list.group_id != attr_list.group_id) { return true; } // groups are in conflict - } - - // compare src_ep - if (device.attr_list.isValidSrcEp() && attr_list.isValidSrcEp()) { - if (device.attr_list.src_ep != attr_list.src_ep) { return true; } - } - - // LQI does not count as conflicting - - // parse all other parameters - for (const auto & attr : attr_list) { - const Z_attribute * curr_attr = device.attr_list.findAttribute(attr); - if (nullptr != curr_attr) { - if (!curr_attr->equalsVal(attr)) { - return true; // the value already exists and is different - conflict! - } - } - } - return false; -} - -void Z_Devices::jsonAppend(uint16_t shortaddr, const Z_attribute_list &attr_list) { - Z_Device & device = getShortAddr(shortaddr); - device.attr_list.mergeList(attr_list); -} - -void Z_Devices::jsonPublishFlush(uint16_t shortaddr) { - Z_Device & device = getShortAddr(shortaddr); - if (!device.valid()) { return; } // safeguard - Z_attribute_list &attr_list = device.attr_list; - - if (!attr_list.isEmpty()) { - const char * fname = zigbee_devices.getFriendlyName(shortaddr); - bool use_fname = (Settings.flag4.zigbee_use_names) && (fname); // should we replace shortaddr with friendlyname? - - // save parameters is global variables to be used by Rules - gZbLastMessage.device = shortaddr; // %zbdevice% - gZbLastMessage.groupaddr = attr_list.group_id; // %zbgroup% - gZbLastMessage.endpoint = attr_list.src_ep; // %zbendpoint% - - mqtt_data[0] = 0; // clear string - // Do we prefix with `ZbReceived`? - if (!Settings.flag4.remove_zbreceived) { - Response_P(PSTR("{\"" D_JSON_ZIGBEE_RECEIVED "\":")); - } - // What key do we use, shortaddr or name? - if (use_fname) { - Response_P(PSTR("%s{\"%s\":{"), mqtt_data, fname); - } else { - Response_P(PSTR("%s{\"0x%04X\":{"), mqtt_data, shortaddr); - } - // Add "Device":"0x...." - Response_P(PSTR("%s\"" D_JSON_ZIGBEE_DEVICE "\":\"0x%04X\","), mqtt_data, shortaddr); - // Add "Name":"xxx" if name is present - if (fname) { - Response_P(PSTR("%s\"" D_JSON_ZIGBEE_NAME "\":\"%s\","), mqtt_data, EscapeJSONString(fname).c_str()); - } - // Add all other attributes - Response_P(PSTR("%s%s}}"), mqtt_data, attr_list.toString().c_str()); - - if (!Settings.flag4.remove_zbreceived) { - Response_P(PSTR("%s}"), mqtt_data); - } - // AddLog_P2(LOG_LEVEL_INFO, PSTR(">>> %s"), mqtt_data); // TODO - attr_list.reset(); // clear the attributes - - if (Settings.flag4.zigbee_distinct_topics) { - if (Settings.flag4.zb_topic_fname && fname) { - //Clean special characters and check size of friendly name - char stemp[TOPSZ]; - strlcpy(stemp, (!strlen(fname)) ? MQTT_TOPIC : fname, sizeof(stemp)); - MakeValidMqtt(0, stemp); - //Create topic with Prefix3 and cleaned up friendly name - char frtopic[TOPSZ]; - snprintf_P(frtopic, sizeof(frtopic), PSTR("%s/%s/" D_RSLT_SENSOR), SettingsText(SET_MQTTPREFIX3), stemp); - MqttPublish(frtopic, Settings.flag.mqtt_sensor_retain); - } else { - char subtopic[16]; - snprintf_P(subtopic, sizeof(subtopic), PSTR("%04X/" D_RSLT_SENSOR), shortaddr); - MqttPublishPrefixTopic_P(TELE, subtopic, Settings.flag.mqtt_sensor_retain); - } - } else { - MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain); - } - XdrvRulesProcess(); // apply rules - } -} - -void Z_Devices::jsonPublishNow(uint16_t shortaddr, Z_attribute_list &attr_list) { - jsonPublishFlush(shortaddr); // flush any previous buffer - jsonAppend(shortaddr, attr_list); - jsonPublishFlush(shortaddr); // publish now -} - -void Z_Devices::dirty(void) { - _saveTimer = kZigbeeSaveDelaySeconds * 1000 + millis(); -} -void Z_Devices::clean(void) { - _saveTimer = 0; -} - -// Parse the command parameters for either: -// - a short address starting with "0x", example: 0x1234 -// - a long address starting with "0x", example: 0x7CB03EBB0A0292DD -// - a number 0..99, the index number in ZigbeeStatus -// - a friendly name, between quotes, example: "Room_Temp" -uint16_t Z_Devices::parseDeviceParam(const char * param, bool short_must_be_known) const { - if (nullptr == param) { return BAD_SHORTADDR; } - size_t param_len = strlen(param); - char dataBuf[param_len + 1]; - strcpy(dataBuf, param); - RemoveSpace(dataBuf); - uint16_t shortaddr = BAD_SHORTADDR; // start with unknown - - if (strlen(dataBuf) < 4) { - // simple number 0..99 - if ((XdrvMailbox.payload > 0) && (XdrvMailbox.payload <= 99)) { - shortaddr = zigbee_devices.isKnownIndex(XdrvMailbox.payload - 1); - } - } else if ((dataBuf[0] == '0') && ((dataBuf[1] == 'x') || (dataBuf[1] == 'X'))) { - // starts with 0x - if (strlen(dataBuf) < 18) { - // expect a short address - shortaddr = strtoull(dataBuf, nullptr, 0); - if (short_must_be_known) { - shortaddr = zigbee_devices.findShortAddr(shortaddr).shortaddr; // if not found, it reverts to the unknown_device with address BAD_SHORTADDR - } - // else we don't check if it's already registered to force unregistered devices - } else { - // expect a long address - uint64_t longaddr = strtoull(dataBuf, nullptr, 0); - shortaddr = zigbee_devices.isKnownLongAddr(longaddr); - } - } else { - // expect a Friendly Name - shortaddr = zigbee_devices.isKnownFriendlyName(dataBuf); - } - - return shortaddr; -} - -// Display the tracked status for a light -String Z_Devices::dumpLightState(uint16_t shortaddr) const { - Z_attribute_list attr_list; - char hex[8]; - - const Z_Device & device = findShortAddr(shortaddr); - const char * fname = getFriendlyName(shortaddr); - bool use_fname = (Settings.flag4.zigbee_use_names) && (fname); // should we replace shortaddr with friendlyname? - snprintf_P(hex, sizeof(hex), PSTR("0x%04X"), shortaddr); - - attr_list.addAttribute(F(D_JSON_ZIGBEE_DEVICE)).setStr(hex); - if (fname) { - attr_list.addAttribute(F(D_JSON_ZIGBEE_NAME)).setStr(fname); - } - - if (foundDevice(device)) { - // expose the last known status of the bulb, for Hue integration - attr_list.addAttribute(F(D_JSON_ZIGBEE_LIGHT)).setInt(getHueBulbtype(shortaddr)); // sign extend, 0xFF changed as -1 - // dump all known values - attr_list.addAttribute(F("Reachable")).setBool(device.getReachable()); - if (device.validPower()) { attr_list.addAttribute(F("Power")).setUInt(device.getPower()); } - if (device.validDimmer()) { attr_list.addAttribute(F("Dimmer")).setUInt(device.dimmer); } - if (device.validColormode()) { attr_list.addAttribute(F("Colormode")).setUInt(device.colormode); } - if (device.validCT()) { attr_list.addAttribute(F("CT")).setUInt(device.ct); } - if (device.validSat()) { attr_list.addAttribute(F("Sat")).setUInt(device.sat); } - if (device.validHue()) { attr_list.addAttribute(F("Hue")).setUInt(device.hue); } - if (device.validX()) { attr_list.addAttribute(F("X")).setUInt(device.x); } - if (device.validY()) { attr_list.addAttribute(F("Y")).setUInt(device.y); } - } - - Z_attribute_list attr_list_root; - Z_attribute * attr_root; - if (use_fname) { - attr_root = &attr_list_root.addAttribute(fname); - } else { - attr_root = &attr_list_root.addAttribute(hex); - } - attr_root->setStrRaw(attr_list.toString(true).c_str()); - return attr_list_root.toString(true); -} - -// Dump the internal memory of Zigbee devices -// Mode = 1: simple dump of devices addresses -// Mode = 2: simple dump of devices addresses and names, endpoints, light -String Z_Devices::dump(uint32_t dump_mode, uint16_t status_shortaddr) const { - Z_json_array json_arr; - - for (const auto & device : _devices) { - uint16_t shortaddr = device.shortaddr; - char hex[22]; - - // ignore non-current device, if device specified - if ((BAD_SHORTADDR != status_shortaddr) && (status_shortaddr != shortaddr)) { continue; } - - Z_attribute_list attr_list; - - snprintf_P(hex, sizeof(hex), PSTR("0x%04X"), shortaddr); - attr_list.addAttribute(F(D_JSON_ZIGBEE_DEVICE)).setStr(hex); - - if (device.friendlyName > 0) { - attr_list.addAttribute(F(D_JSON_ZIGBEE_NAME)).setStr(device.friendlyName); - } - - if (2 <= dump_mode) { - hex[0] = '0'; // prefix with '0x' - hex[1] = 'x'; - Uint64toHex(device.longaddr, &hex[2], 64); - attr_list.addAttribute(F("IEEEAddr")).setStr(hex); - if (device.modelId) { - attr_list.addAttribute(F(D_JSON_MODEL D_JSON_ID)).setStr(device.modelId); - } - int8_t bulbtype = getHueBulbtype(shortaddr); - if (bulbtype >= 0) { - attr_list.addAttribute(F(D_JSON_ZIGBEE_LIGHT)).setInt(bulbtype); // sign extend, 0xFF changed as -1 - } - if (device.manufacturerId) { - attr_list.addAttribute(F("Manufacturer")).setStr(device.manufacturerId); - } - Z_json_array arr_ep; - for (uint32_t i = 0; i < endpoints_max; i++) { - uint8_t endpoint = device.endpoints[i]; - if (0x00 == endpoint) { break; } - arr_ep.add(endpoint); - } - attr_list.addAttribute(F("Endpoints")).setStrRaw(arr_ep.toString().c_str()); - } - json_arr.addStrRaw(attr_list.toString(true).c_str()); - } - return json_arr.toString(); -} - -// Restore a single device configuration based on json export -// Input: json element as expported by `ZbStatus2`` -// Mandatory attribue: `Device` -// -// Returns: -// 0 : Ok -// <0 : Error -// -// Ex: {"Device":"0x5ADF","Name":"IKEA_Light","IEEEAddr":"0x90FD9FFFFE03B051","ModelId":"TRADFRI bulb E27 WS opal 980lm","Manufacturer":"IKEA of Sweden","Endpoints":["0x01","0xF2"]} -int32_t Z_Devices::deviceRestore(JsonParserObject json) { - - // params - uint16_t device = 0x0000; // 0x0000 is coordinator so considered invalid - uint64_t ieeeaddr = 0x0000000000000000LL; // 0 means unknown - const char * modelid = nullptr; - const char * manufid = nullptr; - const char * friendlyname = nullptr; - int8_t bulbtype = -1; - size_t endpoints_len = 0; - - // read mandatory "Device" - JsonParserToken val_device = json[PSTR("Device")]; - if (val_device) { - device = (uint32_t) val_device.getUInt(device); - } else { - return -1; // missing "Device" attribute - } - - ieeeaddr = json.getULong(PSTR("IEEEAddr"), ieeeaddr); // read "IEEEAddr" 64 bits in format "0x0000000000000000" - friendlyname = json.getStr(PSTR("Name"), nullptr); // read "Name" - modelid = json.getStr(PSTR("ModelId"), nullptr); - manufid = json.getStr(PSTR("Manufacturer"), nullptr); - JsonParserToken tok_bulbtype = json[PSTR(D_JSON_ZIGBEE_LIGHT)]; - - // update internal device information - updateDevice(device, ieeeaddr); - if (modelid) { setModelId(device, modelid); } - if (manufid) { setManufId(device, manufid); } - if (friendlyname) { setFriendlyName(device, friendlyname); } - if (tok_bulbtype) { setHueBulbtype(device, tok_bulbtype.getInt()); } - - // read "Endpoints" - JsonParserToken val_endpoints = json[PSTR("Endpoints")]; - if (val_endpoints.isArray()) { - JsonParserArray arr_ep = JsonParserArray(val_endpoints); - clearEndpoints(device); // clear even if array is empty - for (auto ep_elt : arr_ep) { - uint8_t ep = ep_elt.getUInt(); - if (ep) { addEndpoint(device, ep); } - } - } - - return 0; -} #endif // USE_ZIGBEE diff --git a/tasmota/xdrv_23_zigbee_2a_devices_impl.ino b/tasmota/xdrv_23_zigbee_2a_devices_impl.ino new file mode 100644 index 000000000..3b7afcf9b --- /dev/null +++ b/tasmota/xdrv_23_zigbee_2a_devices_impl.ino @@ -0,0 +1,779 @@ +/* + xdrv_23_zigbee_2a_devices_impl.ino - zigbee support for Tasmota + + Copyright (C) 2020 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 + +/*********************************************************************************************\ + * Implementation +\*********************************************************************************************/ + +// +// Create a new Z_Device entry in _devices. Only to be called if you are sure that no +// entry with same shortaddr or longaddr exists. +// +Z_Device & Z_Devices::createDeviceEntry(uint16_t shortaddr, uint64_t longaddr) { + if ((BAD_SHORTADDR == shortaddr) && !longaddr) { return (Z_Device&) device_unk; } // it is not legal to create this entry + Z_Device device(shortaddr, longaddr); + + dirty(); + return _devices.addHead(device); +} + +void Z_Devices::freeDeviceEntry(Z_Device *device) { + if (device->manufacturerId) { free(device->manufacturerId); } + if (device->modelId) { free(device->modelId); } + if (device->friendlyName) { free(device->friendlyName); } + free(device); +} + +// +// Scan all devices to find a corresponding shortaddr +// Looks info device.shortaddr entry +// In: +// shortaddr (not BAD_SHORTADDR) +// Out: +// reference to device, or to device_unk if not found +// (use foundDevice() to check if found) +Z_Device & Z_Devices::findShortAddr(uint16_t shortaddr) { + for (auto & elem : _devices) { + if (elem.shortaddr == shortaddr) { return elem; } + } + return (Z_Device&) device_unk; +} +const Z_Device & Z_Devices::findShortAddr(uint16_t shortaddr) const { + for (const auto & elem : _devices) { + if (elem.shortaddr == shortaddr) { return elem; } + } + return device_unk; +} +// +// Scan all devices to find a corresponding longaddr +// Looks info device.longaddr entry +// In: +// longaddr (non null) +// Out: +// index in _devices of entry, -1 if not found +// +Z_Device & Z_Devices::findLongAddr(uint64_t longaddr) { + if (!longaddr) { return (Z_Device&) device_unk; } + for (auto &elem : _devices) { + if (elem.longaddr == longaddr) { return elem; } + } + return (Z_Device&) device_unk; +} +const Z_Device & Z_Devices::findLongAddr(uint64_t longaddr) const { + if (!longaddr) { return device_unk; } + for (const auto &elem : _devices) { + if (elem.longaddr == longaddr) { return elem; } + } + return device_unk; +} +// +// Scan all devices to find a corresponding friendlyNme +// Looks info device.friendlyName entry +// In: +// friendlyName (null terminated, should not be empty) +// Out: +// index in _devices of entry, -1 if not found +// +int32_t Z_Devices::findFriendlyName(const char * name) const { + if (!name) { return -1; } // if pointer is null + size_t name_len = strlen(name); + int32_t found = 0; + if (name_len) { + for (auto &elem : _devices) { + if (elem.friendlyName) { + if (strcasecmp(elem.friendlyName, name) == 0) { return found; } + } + found++; + } + } + return -1; +} + +uint16_t Z_Devices::isKnownLongAddr(uint64_t longaddr) const { + const Z_Device & device = findLongAddr(longaddr); + if (foundDevice(device)) { + return device.shortaddr; // can be zero, if not yet registered + } else { + return BAD_SHORTADDR; + } +} + +uint16_t Z_Devices::isKnownIndex(uint32_t index) const { + if (index < devicesSize()) { + const Z_Device & device = devicesAt(index); + return device.shortaddr; + } else { + return BAD_SHORTADDR; + } +} + +uint16_t Z_Devices::isKnownFriendlyName(const char * name) const { + if ((!name) || (0 == strlen(name))) { return BAD_SHORTADDR; } // Error + int32_t found = findFriendlyName(name); + if (found >= 0) { + const Z_Device & device = devicesAt(found); + return device.shortaddr; // can be zero, if not yet registered + } else { + return BAD_SHORTADDR; + } +} + +uint64_t Z_Devices::getDeviceLongAddr(uint16_t shortaddr) const { + return findShortAddr(shortaddr).longaddr; // if unknown, it reverts to the Unknown device and longaddr is 0x00 +} + +// +// We have a seen a shortaddr on the network, get the corresponding device object +// +Z_Device & Z_Devices::getShortAddr(uint16_t shortaddr) { + if (BAD_SHORTADDR == shortaddr) { return (Z_Device&) device_unk; } // this is not legal + Z_Device & device = findShortAddr(shortaddr); + if (foundDevice(device)) { + return device; + } + return createDeviceEntry(shortaddr, 0); +} + +// find the Device object by its longaddr (unique key if not null) +Z_Device & Z_Devices::getLongAddr(uint64_t longaddr) { + if (!longaddr) { return (Z_Device&) device_unk; } + Z_Device & device = findLongAddr(longaddr); + if (foundDevice(device)) { + return device; + } + return createDeviceEntry(0, longaddr); +} + +// Remove device from list, return true if it was known, false if it was not recorded +bool Z_Devices::removeDevice(uint16_t shortaddr) { + Z_Device & device = findShortAddr(shortaddr); + if (foundDevice(device)) { + _devices.remove(&device); + dirty(); + return true; + } + return false; +} + +// +// We have just seen a device on the network, update the info based on short/long addr +// In: +// shortaddr +// longaddr (both can't be null at the same time) +void Z_Devices::updateDevice(uint16_t shortaddr, uint64_t longaddr) { + Z_Device * s_found = &findShortAddr(shortaddr); // is there already a shortaddr entry + Z_Device * l_found = &findLongAddr(longaddr); // is there already a longaddr entry + + if (foundDevice(*s_found) && foundDevice(*l_found)) { // both shortaddr and longaddr are already registered + if (s_found == l_found) { + } else { // they don't match + // the device with longaddr got a new shortaddr + l_found->shortaddr = shortaddr; // update the shortaddr corresponding to the longaddr + // erase the previous shortaddr + freeDeviceEntry(s_found); + _devices.remove(s_found); + dirty(); + } + } else if (foundDevice(*s_found)) { + // shortaddr already exists but longaddr not + // add the longaddr to the entry + s_found->longaddr = longaddr; + dirty(); + } else if (foundDevice(*l_found)) { + // longaddr entry exists, update shortaddr + l_found->shortaddr = shortaddr; + dirty(); + } else { + // neither short/lonf addr are found. + if ((BAD_SHORTADDR != shortaddr) || longaddr) { + createDeviceEntry(shortaddr, longaddr); + } + } +} + +// +// Clear all endpoints +// +void Z_Devices::clearEndpoints(uint16_t shortaddr) { + Z_Device &device = getShortAddr(shortaddr); + for (uint32_t i = 0; i < endpoints_max; i++) { + device.endpoints[i] = 0; + // no dirty here because it doesn't make sense to store it, does it? + } +} + +// +// Add an endpoint to a shortaddr +// +void Z_Devices::addEndpoint(uint16_t shortaddr, uint8_t endpoint) { + if (0x00 == endpoint) { return; } + Z_Device &device = getShortAddr(shortaddr); + + for (uint32_t i = 0; i < endpoints_max; i++) { + if (endpoint == device.endpoints[i]) { + return; // endpoint already there + } + if (0 == device.endpoints[i]) { + device.endpoints[i] = endpoint; + dirty(); + return; + } + } +} + +// +// Count the number of known endpoints +// +uint32_t Z_Devices::countEndpoints(uint16_t shortaddr) const { + uint32_t count_ep = 0; + const Z_Device & device =findShortAddr(shortaddr); + if (!foundDevice(device)) return 0; + + for (uint32_t i = 0; i < endpoints_max; i++) { + if (0 != device.endpoints[i]) { + count_ep++; + } + } + return count_ep; +} + +// Find the first endpoint of the device +uint8_t Z_Devices::findFirstEndpoint(uint16_t shortaddr) const { + // When in router of end-device mode, the coordinator was not probed, in this case always talk to endpoint 1 + if (0x0000 == shortaddr) { return 1; } + return findShortAddr(shortaddr).endpoints[0]; // returns 0x00 if no endpoint +} + +void Z_Devices::setStringAttribute(char*& attr, const char * str) { + if (nullptr == str) { return; } // ignore a null parameter + size_t str_len = strlen(str); + + if ((nullptr == attr) && (0 == str_len)) { return; } // if both empty, don't do anything + if (attr) { + // we already have a value + if (strcmp(attr, str) != 0) { + // new value + free(attr); // free previous value + attr = nullptr; + } else { + return; // same value, don't change anything + } + } + if (str_len) { + attr = (char*) malloc(str_len + 1); + strlcpy(attr, str, str_len + 1); + } + dirty(); +} + +// +// Sets the ManufId for a device. +// No action taken if the device does not exist. +// Inputs: +// - shortaddr: 16-bits short address of the device. No action taken if the device is unknown +// - str: string pointer, if nullptr it is considered as empty string +// Impact: +// - Any actual change in ManufId (i.e. setting a different value) triggers a `dirty()` and saving to Flash +// +void Z_Devices::setManufId(uint16_t shortaddr, const char * str) { + setStringAttribute(getShortAddr(shortaddr).manufacturerId, str); +} + +void Z_Devices::setModelId(uint16_t shortaddr, const char * str) { + setStringAttribute(getShortAddr(shortaddr).modelId, str); +} + +void Z_Devices::setFriendlyName(uint16_t shortaddr, const char * str) { + setStringAttribute(getShortAddr(shortaddr).friendlyName, str); +} + + +void Z_Devices::setReachable(uint16_t shortaddr, bool reachable) { + getShortAddr(shortaddr).setReachable(reachable); +} + +void Z_Devices::setLQI(uint16_t shortaddr, uint8_t lqi) { + if (shortaddr == localShortAddr) { return; } + getShortAddr(shortaddr).lqi = lqi; +} + +void Z_Devices::setLastSeenNow(uint16_t shortaddr) { + if (shortaddr == localShortAddr) { return; } + // Only update time if after 2020-01-01 0000. + // Fixes issue where zigbee device pings before WiFi/NTP has set utc_time + // to the correct time, and "last seen" calculations are based on the + // pre-corrected last_seen time and the since-corrected utc_time. + if (Rtc.utc_time < 1577836800) { return; } + getShortAddr(shortaddr).last_seen = Rtc.utc_time; +} + + +void Z_Devices::setBatteryPercent(uint16_t shortaddr, uint8_t bp) { + getShortAddr(shortaddr).batterypercent = bp; +} + +// get the next sequance number for the device, or use the global seq number if device is unknown +uint8_t Z_Devices::getNextSeqNumber(uint16_t shortaddr) { + Z_Device & device = findShortAddr(shortaddr); + if (foundDevice(device)) { + device.seqNumber += 1; + return device.seqNumber; + } else { + _seqNumber += 1; + return _seqNumber; + } +} + +// General Zigbee device profile support +void Z_Devices::setLightProfile(uint16_t shortaddr, uint8_t light_profile) { + Z_Device &device = getShortAddr(shortaddr); + if (device.setLightChannels(light_profile)) { + dirty(); + } +} + +// Returns the device profile or 0xFF if the device or profile is unknown +uint8_t Z_Devices::getLightProfile(uint16_t shortaddr) const { + const Z_Device &device = findShortAddr(shortaddr); + return device.getLightChannels(); +} + +int8_t Z_Devices::getHueBulbtype(uint16_t shortaddr) const { + int8_t light_profile = getLightProfile(shortaddr); + if (0x00 == (light_profile & 0xF0)) { + return (light_profile & 0x07); + } else { + // not a bulb + return -1; + } +} + +void Z_Devices::hideHueBulb(uint16_t shortaddr, bool hidden) { + Z_Device &device = getShortAddr(shortaddr); + if (device.hidden != hidden) { + device.hidden = hidden; + dirty(); + } +} +// true if device is not knwon or not a bulb - it wouldn't make sense to publish a non-bulb +bool Z_Devices::isHueBulbHidden(uint16_t shortaddr) const { + const Z_Device & device = findShortAddr(shortaddr); + if (foundDevice(device)) { + return device.hidden; + } + return true; // Fallback - Device is considered as hidden +} + +// Deferred actions +// Parse for a specific category, of all deferred for a device if category == 0xFF +// Only with specific cluster number or for all clusters if cluster == 0xFFFF +void Z_Devices::resetTimersForDevice(uint16_t shortaddr, uint16_t groupaddr, uint8_t category, uint16_t cluster, uint8_t endpoint) { + // iterate the list of deferred, and remove any linked to the shortaddr + for (auto & defer : _deferred) { + if ((defer.shortaddr == shortaddr) && (defer.groupaddr == groupaddr)) { + if ((0xFF == category) || (defer.category == category)) { + if ((0xFFFF == cluster) || (defer.cluster == cluster)) { + if ((0xFF == endpoint) || (defer.endpoint == endpoint)) { + _deferred.remove(&defer); + } + } + } + } + } +} + +// Set timer for a specific device +void Z_Devices::setTimer(uint16_t shortaddr, uint16_t groupaddr, uint32_t wait_ms, uint16_t cluster, uint8_t endpoint, uint8_t category, uint32_t value, Z_DeviceTimer func) { + // First we remove any existing timer for same device in same category, except for category=0x00 (they need to happen anyway) + if (category >= Z_CLEAR_DEVICE) { // if category == 0, we leave all previous timers + resetTimersForDevice(shortaddr, groupaddr, category, category >= Z_CLEAR_DEVICE_CLUSTER ? cluster : 0xFFFF, category >= Z_CLEAR_DEVICE_CLUSTER_ENDPOINT ? endpoint : 0xFF); // remove any cluster + } + + // Now create the new timer + Z_Deferred & deferred = _deferred.addHead(); + deferred = { wait_ms + millis(), // timer + shortaddr, + groupaddr, + cluster, + endpoint, + category, + value, + func }; +} + +// Set timer after the already queued events +// I.e. the wait_ms is not counted from now, but from the last event queued, which is 'now' or in the future +void Z_Devices::queueTimer(uint16_t shortaddr, uint16_t groupaddr, uint32_t wait_ms, uint16_t cluster, uint8_t endpoint, uint8_t category, uint32_t value, Z_DeviceTimer func) { + Z_Device & device = getShortAddr(shortaddr); + uint32_t now_millis = millis(); + if (TimeReached(device.defer_last_message_sent)) { + device.defer_last_message_sent = now_millis; + } + // defer_last_message_sent equals now or a value in the future + device.defer_last_message_sent += wait_ms; + + // for queueing we don't clear the backlog, so we force category to Z_CAT_ALWAYS + setTimer(shortaddr, groupaddr, (device.defer_last_message_sent - now_millis), cluster, endpoint, Z_CAT_ALWAYS, value, func); +} + +// Run timer at each tick +// WARNING: don't set a new timer within a running timer, this causes memory corruption +void Z_Devices::runTimer(void) { + // visit all timers + for (auto & defer : _deferred) { + uint32_t timer = defer.timer; + if (TimeReached(timer)) { + (*defer.func)(defer.shortaddr, defer.groupaddr, defer.cluster, defer.endpoint, defer.value); + _deferred.remove(&defer); + } + } + + // check if we need to save to Flash + if ((_saveTimer) && TimeReached(_saveTimer)) { + saveZigbeeDevices(); + _saveTimer = 0; + } +} + +// does the new payload conflicts with the existing payload, i.e. values would be overwritten +// true - one attribute (except LinkQuality) woudl be lost, there is conflict +// false - new attributes can be safely added +bool Z_Devices::jsonIsConflict(uint16_t shortaddr, const Z_attribute_list &attr_list) const { + const Z_Device & device = findShortAddr(shortaddr); + + if (!foundDevice(device)) { return false; } + if (attr_list.isEmpty()) { + return false; // if no previous value, no conflict + } + + // compare groups + if (device.attr_list.isValidGroupId() && attr_list.isValidGroupId()) { + if (device.attr_list.group_id != attr_list.group_id) { return true; } // groups are in conflict + } + + // compare src_ep + if (device.attr_list.isValidSrcEp() && attr_list.isValidSrcEp()) { + if (device.attr_list.src_ep != attr_list.src_ep) { return true; } + } + + // LQI does not count as conflicting + + // parse all other parameters + for (const auto & attr : attr_list) { + const Z_attribute * curr_attr = device.attr_list.findAttribute(attr); + if (nullptr != curr_attr) { + if (!curr_attr->equalsVal(attr)) { + return true; // the value already exists and is different - conflict! + } + } + } + return false; +} + +void Z_Devices::jsonAppend(uint16_t shortaddr, const Z_attribute_list &attr_list) { + Z_Device & device = getShortAddr(shortaddr); + device.attr_list.mergeList(attr_list); +} + +void Z_Devices::jsonPublishFlush(uint16_t shortaddr) { + Z_Device & device = getShortAddr(shortaddr); + if (!device.valid()) { return; } // safeguard + Z_attribute_list &attr_list = device.attr_list; + + if (!attr_list.isEmpty()) { + const char * fname = zigbee_devices.getFriendlyName(shortaddr); + bool use_fname = (Settings.flag4.zigbee_use_names) && (fname); // should we replace shortaddr with friendlyname? + + // save parameters is global variables to be used by Rules + gZbLastMessage.device = shortaddr; // %zbdevice% + gZbLastMessage.groupaddr = attr_list.group_id; // %zbgroup% + gZbLastMessage.endpoint = attr_list.src_ep; // %zbendpoint% + + mqtt_data[0] = 0; // clear string + // Do we prefix with `ZbReceived`? + if (!Settings.flag4.remove_zbreceived) { + Response_P(PSTR("{\"" D_JSON_ZIGBEE_RECEIVED "\":")); + } + // What key do we use, shortaddr or name? + if (use_fname) { + Response_P(PSTR("%s{\"%s\":{"), mqtt_data, fname); + } else { + Response_P(PSTR("%s{\"0x%04X\":{"), mqtt_data, shortaddr); + } + // Add "Device":"0x...." + Response_P(PSTR("%s\"" D_JSON_ZIGBEE_DEVICE "\":\"0x%04X\","), mqtt_data, shortaddr); + // Add "Name":"xxx" if name is present + if (fname) { + Response_P(PSTR("%s\"" D_JSON_ZIGBEE_NAME "\":\"%s\","), mqtt_data, EscapeJSONString(fname).c_str()); + } + // Add all other attributes + Response_P(PSTR("%s%s}}"), mqtt_data, attr_list.toString().c_str()); + + if (!Settings.flag4.remove_zbreceived) { + Response_P(PSTR("%s}"), mqtt_data); + } + attr_list.reset(); // clear the attributes + + if (Settings.flag4.zigbee_distinct_topics) { + if (Settings.flag4.zb_topic_fname && fname) { + //Clean special characters and check size of friendly name + char stemp[TOPSZ]; + strlcpy(stemp, (!strlen(fname)) ? MQTT_TOPIC : fname, sizeof(stemp)); + MakeValidMqtt(0, stemp); + //Create topic with Prefix3 and cleaned up friendly name + char frtopic[TOPSZ]; + snprintf_P(frtopic, sizeof(frtopic), PSTR("%s/%s/" D_RSLT_SENSOR), SettingsText(SET_MQTTPREFIX3), stemp); + MqttPublish(frtopic, Settings.flag.mqtt_sensor_retain); + } else { + char subtopic[16]; + snprintf_P(subtopic, sizeof(subtopic), PSTR("%04X/" D_RSLT_SENSOR), shortaddr); + MqttPublishPrefixTopic_P(TELE, subtopic, Settings.flag.mqtt_sensor_retain); + } + } else { + MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain); + } + XdrvRulesProcess(); // apply rules + } +} + +void Z_Devices::jsonPublishNow(uint16_t shortaddr, Z_attribute_list &attr_list) { + jsonPublishFlush(shortaddr); // flush any previous buffer + jsonAppend(shortaddr, attr_list); + jsonPublishFlush(shortaddr); // publish now +} + +void Z_Devices::dirty(void) { + _saveTimer = kZigbeeSaveDelaySeconds * 1000 + millis(); +} +void Z_Devices::clean(void) { + _saveTimer = 0; +} + +// Parse the command parameters for either: +// - a short address starting with "0x", example: 0x1234 +// - a long address starting with "0x", example: 0x7CB03EBB0A0292DD +// - a number 0..99, the index number in ZigbeeStatus +// - a friendly name, between quotes, example: "Room_Temp" +uint16_t Z_Devices::parseDeviceParam(const char * param, bool short_must_be_known) const { + if (nullptr == param) { return BAD_SHORTADDR; } + size_t param_len = strlen(param); + char dataBuf[param_len + 1]; + strcpy(dataBuf, param); + RemoveSpace(dataBuf); + uint16_t shortaddr = BAD_SHORTADDR; // start with unknown + + if (strlen(dataBuf) < 4) { + // simple number 0..99 + if ((XdrvMailbox.payload > 0) && (XdrvMailbox.payload <= 99)) { + shortaddr = zigbee_devices.isKnownIndex(XdrvMailbox.payload - 1); + } + } else if ((dataBuf[0] == '0') && ((dataBuf[1] == 'x') || (dataBuf[1] == 'X'))) { + // starts with 0x + if (strlen(dataBuf) < 18) { + // expect a short address + shortaddr = strtoull(dataBuf, nullptr, 0); + if (short_must_be_known) { + shortaddr = zigbee_devices.findShortAddr(shortaddr).shortaddr; // if not found, it reverts to the unknown_device with address BAD_SHORTADDR + } + // else we don't check if it's already registered to force unregistered devices + } else { + // expect a long address + uint64_t longaddr = strtoull(dataBuf, nullptr, 0); + shortaddr = zigbee_devices.isKnownLongAddr(longaddr); + } + } else { + // expect a Friendly Name + shortaddr = zigbee_devices.isKnownFriendlyName(dataBuf); + } + + return shortaddr; +} + +// Display the tracked status for a light +String Z_Devices::dumpLightState(uint16_t shortaddr) const { + Z_attribute_list attr_list; + char hex[8]; + + const Z_Device & device = findShortAddr(shortaddr); + const char * fname = getFriendlyName(shortaddr); + bool use_fname = (Settings.flag4.zigbee_use_names) && (fname); // should we replace shortaddr with friendlyname? + snprintf_P(hex, sizeof(hex), PSTR("0x%04X"), shortaddr); + + attr_list.addAttribute(F(D_JSON_ZIGBEE_DEVICE)).setStr(hex); + if (fname) { + attr_list.addAttribute(F(D_JSON_ZIGBEE_NAME)).setStr(fname); + } + + if (foundDevice(device)) { + // dump all known values + attr_list.addAttribute(F("Reachable")).setBool(device.getReachable()); + if (device.validPower()) { attr_list.addAttribute(F("Power")).setUInt(device.getPower()); } + Z_Data_Light::toAttributes(attr_list, device.data.find(0)); + } + + Z_attribute_list attr_list_root; + Z_attribute * attr_root; + if (use_fname) { + attr_root = &attr_list_root.addAttribute(fname); + } else { + attr_root = &attr_list_root.addAttribute(hex); + } + attr_root->setStrRaw(attr_list.toString(true).c_str()); + return attr_list_root.toString(true); +} + +// Dump the internal memory of Zigbee devices +// Mode = 1: simple dump of devices addresses +// Mode = 2: simple dump of devices addresses and names, endpoints, light +String Z_Devices::dump(uint32_t dump_mode, uint16_t status_shortaddr) const { + Z_json_array json_arr; + + for (const auto & device : _devices) { + uint16_t shortaddr = device.shortaddr; + char hex[22]; + + // ignore non-current device, if device specified + if ((BAD_SHORTADDR != status_shortaddr) && (status_shortaddr != shortaddr)) { continue; } + + Z_attribute_list attr_list; + + snprintf_P(hex, sizeof(hex), PSTR("0x%04X"), shortaddr); + attr_list.addAttribute(F(D_JSON_ZIGBEE_DEVICE)).setStr(hex); + + if (device.friendlyName > 0) { + attr_list.addAttribute(F(D_JSON_ZIGBEE_NAME)).setStr(device.friendlyName); + } + + if (2 <= dump_mode) { + hex[0] = '0'; // prefix with '0x' + hex[1] = 'x'; + Uint64toHex(device.longaddr, &hex[2], 64); + attr_list.addAttribute(F("IEEEAddr")).setStr(hex); + if (device.modelId) { + attr_list.addAttribute(F(D_JSON_MODEL D_JSON_ID)).setStr(device.modelId); + } + int8_t bulbtype = getHueBulbtype(shortaddr); + if (bulbtype >= 0) { + attr_list.addAttribute(F(D_JSON_ZIGBEE_LIGHT)).setInt(bulbtype); // sign extend, 0xFF changed as -1 + } + if (device.manufacturerId) { + attr_list.addAttribute(F("Manufacturer")).setStr(device.manufacturerId); + } + Z_json_array arr_ep; + for (uint32_t i = 0; i < endpoints_max; i++) { + uint8_t endpoint = device.endpoints[i]; + if (0x00 == endpoint) { break; } + arr_ep.add(endpoint); + } + attr_list.addAttribute(F("Endpoints")).setStrRaw(arr_ep.toString().c_str()); + } + json_arr.addStrRaw(attr_list.toString(true).c_str()); + } + return json_arr.toString(); +} + +// Restore a single device configuration based on json export +// Input: json element as expported by `ZbStatus2`` +// Mandatory attribue: `Device` +// +// Returns: +// 0 : Ok +// <0 : Error +// +// Ex: {"Device":"0x5ADF","Name":"IKEA_Light","IEEEAddr":"0x90FD9FFFFE03B051","ModelId":"TRADFRI bulb E27 WS opal 980lm","Manufacturer":"IKEA of Sweden","Endpoints":["0x01","0xF2"]} +int32_t Z_Devices::deviceRestore(JsonParserObject json) { + + // params + uint16_t device = 0x0000; // 0x0000 is coordinator so considered invalid + uint64_t ieeeaddr = 0x0000000000000000LL; // 0 means unknown + const char * modelid = nullptr; + const char * manufid = nullptr; + const char * friendlyname = nullptr; + int8_t bulbtype = -1; + size_t endpoints_len = 0; + + // read mandatory "Device" + JsonParserToken val_device = json[PSTR("Device")]; + if (val_device) { + device = (uint32_t) val_device.getUInt(device); + } else { + return -1; // missing "Device" attribute + } + + ieeeaddr = json.getULong(PSTR("IEEEAddr"), ieeeaddr); // read "IEEEAddr" 64 bits in format "0x0000000000000000" + friendlyname = json.getStr(PSTR("Name"), nullptr); // read "Name" + modelid = json.getStr(PSTR("ModelId"), nullptr); + manufid = json.getStr(PSTR("Manufacturer"), nullptr); + JsonParserToken tok_bulbtype = json[PSTR(D_JSON_ZIGBEE_LIGHT)]; + + // update internal device information + updateDevice(device, ieeeaddr); + if (modelid) { setModelId(device, modelid); } + if (manufid) { setManufId(device, manufid); } + if (friendlyname) { setFriendlyName(device, friendlyname); } + if (tok_bulbtype) { setLightProfile(device, tok_bulbtype.getInt()); } + + // read "Endpoints" + JsonParserToken val_endpoints = json[PSTR("Endpoints")]; + if (val_endpoints.isArray()) { + JsonParserArray arr_ep = JsonParserArray(val_endpoints); + clearEndpoints(device); // clear even if array is empty + for (auto ep_elt : arr_ep) { + uint8_t ep = ep_elt.getUInt(); + if (ep) { addEndpoint(device, ep); } + } + } + + return 0; +} + +Z_Data_Light & Z_Devices::getLight(uint16_t shortaddr) { + return getShortAddr(shortaddr).data.get(); +} + +/*********************************************************************************************\ + * Export device specific attributes to ZbData +\*********************************************************************************************/ +void Z_Device::toAttributes(Z_attribute_list & attr_list) const { + if (validLqi()) { attr_list.addAttribute(PSTR(D_CMND_ZIGBEE_LINKQUALITY)).setUInt(lqi); } + if (validBatteryPercent()) { attr_list.addAttribute(PSTR("BatteryPercentage")).setUInt(batterypercent); } + if (validLastSeen()) { attr_list.addAttribute(PSTR("LastSeen")).setUInt(last_seen); } +} + +/*********************************************************************************************\ + * Device specific data handlers +\*********************************************************************************************/ +void Z_Device::setPower(bool power_on, uint8_t ep) { + data.get(ep).setPower(power_on); +} + +bool Z_Device::validPower(uint8_t ep) const { + const Z_Data_OnOff & onoff = data.find(ep); + return (&onoff != nullptr); +} + +bool Z_Device::getPower(uint8_t ep) const { + const Z_Data_OnOff & onoff = data.find(ep); + if (&onoff != nullptr) return onoff.getPower(); + return false; +} + +#endif // USE_ZIGBEE diff --git a/tasmota/xdrv_23_zigbee_3_hue.ino b/tasmota/xdrv_23_zigbee_3_hue.ino index 1f4b18e19..a42695fd2 100644 --- a/tasmota/xdrv_23_zigbee_3_hue.ino +++ b/tasmota/xdrv_23_zigbee_3_hue.ino @@ -29,23 +29,29 @@ void HueLightStatus1Zigbee(uint16_t shortaddr, uint8_t local_light_subtype, Stri "\"effect\":\"none\"," "\"reachable\":%s}"; - bool power, reachable; - uint8_t colormode, bri, sat; - uint16_t ct, hue; - uint16_t x, y; + bool power = false; + bool reachable = false; + uint8_t colormode = 0xFF; + uint8_t bri = 0xFF; + uint8_t sat = 0xFF; + uint16_t ct = 0xFFFF; + uint16_t hue = 0xFFFF; + uint16_t x = 0xFFFF, y = 0xFFFF; String light_status = ""; uint32_t echo_gen = findEchoGeneration(); // 1 for 1st gen =+ Echo Dot 2nd gen, 2 for 2nd gen and above const Z_Device & device = zigbee_devices.findShortAddr(shortaddr); - // TODO TODO check also validity - bri = device.dimmer; + const Z_Data_Light & light = device.data.find(); + if (&light != nullptr) { + bri = light.getDimmer(); + colormode = light.getColorMode(); + sat = light.getSat(); + ct = light.getCT(); + hue = light.getHue(); + x = light.getX(); + y = light.getY(); + } power = device.getPower(); - colormode = device.colormode; - sat = device.sat; - ct = device.ct; - hue = device.hue; - x = device.x; - y = device.y; reachable = device.getReachable(); if (bri > 254) bri = 254; // Philips Hue bri is between 1 and 254 @@ -148,7 +154,7 @@ void ZigbeeHueGroups(String * lights) { // Power On/Off void ZigbeeHuePower(uint16_t shortaddr, bool power) { zigbeeZCLSendStr(shortaddr, 0, 0, true, 0, 0x0006, power ? 1 : 0, ""); - zigbee_devices.getShortAddr(shortaddr).setPower(power); + zigbee_devices.getShortAddr(shortaddr).setPower(power, 0); } // Dimmer @@ -157,7 +163,7 @@ void ZigbeeHueDimmer(uint16_t shortaddr, uint8_t dimmer) { char param[8]; snprintf_P(param, sizeof(param), PSTR("%02X0A00"), dimmer); zigbeeZCLSendStr(shortaddr, 0, 0, true, 0, 0x0008, 0x04, param); - zigbee_devices.getShortAddr(shortaddr).dimmer = dimmer; + zigbee_devices.getLight(shortaddr).setDimmer(dimmer); } // CT @@ -168,9 +174,9 @@ void ZigbeeHueCT(uint16_t shortaddr, uint16_t ct) { snprintf_P(param, sizeof(param), PSTR("%02X%02X0A00"), ct & 0xFF, ct >> 8); uint8_t colormode = 2; // "ct" zigbeeZCLSendStr(shortaddr, 0, 0, true, 0, 0x0300, 0x0A, param); - Z_Device & device = zigbee_devices.getShortAddr(shortaddr); - device.colormode = colormode; - device.ct = ct; + Z_Data_Light & light = zigbee_devices.getLight(shortaddr); + light.setColorMode(colormode); + light.setCT(ct); } // XY @@ -181,10 +187,10 @@ void ZigbeeHueXY(uint16_t shortaddr, uint16_t x, uint16_t y) { snprintf_P(param, sizeof(param), PSTR("%02X%02X%02X%02X0A00"), x & 0xFF, x >> 8, y & 0xFF, y >> 8); uint8_t colormode = 1; // "xy" zigbeeZCLSendStr(shortaddr, 0, 0, true, 0, 0x0300, 0x07, param); - Z_Device & device = zigbee_devices.getShortAddr(shortaddr); - device.colormode = colormode; - device.x = x; - device.y = y; + Z_Data_Light & light = zigbee_devices.getLight(shortaddr); + light.setColorMode(colormode); + light.setX(x); + light.setY(y); } // HueSat @@ -195,10 +201,10 @@ void ZigbeeHueHS(uint16_t shortaddr, uint16_t hue, uint8_t sat) { snprintf_P(param, sizeof(param), PSTR("%02X%02X0000"), hue8, sat); uint8_t colormode = 0; // "hs" zigbeeZCLSendStr(shortaddr, 0, 0, true, 0, 0x0300, 0x06, param); - Z_Device device = zigbee_devices.getShortAddr(shortaddr); - device.colormode = colormode; - device.sat = sat; - device.hue = hue; + Z_Data_Light & light = zigbee_devices.getLight(shortaddr); + light.setColorMode(colormode); + light.setSat(sat); + light.setHue(hue); } void ZigbeeHandleHue(uint16_t shortaddr, uint32_t device_id, String &response) { diff --git a/tasmota/xdrv_23_zigbee_4_persistence.ino b/tasmota/xdrv_23_zigbee_4_persistence.ino index 1a1fdc38c..0e688c960 100644 --- a/tasmota/xdrv_23_zigbee_4_persistence.ino +++ b/tasmota/xdrv_23_zigbee_4_persistence.ino @@ -127,7 +127,7 @@ class SBuffer hibernateDevice(const struct Z_Device &device) { buf.add8(0x00); // end of string marker // Zigbee Profile - buf.add8(device.zb_profile); + buf.add8(device.getLightChannels()); // update overall length buf.set8(0, buf.len()); @@ -170,7 +170,6 @@ void hydrateDevices(const SBuffer &buf) { uint32_t k = 0; uint32_t num_devices = buf.get8(k++); -//size_t before = 0; for (uint32_t i = 0; (i < num_devices) && (k < buf_len); i++) { uint32_t dev_record_len = buf.get8(k); @@ -200,7 +199,6 @@ void hydrateDevices(const SBuffer &buf) { // ignore } } -//AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Device 0x%04X Memory3.shrink = %d"), shortaddr, ESP_getFreeHeap()); // parse 3 strings char empty[] = ""; @@ -225,13 +223,12 @@ void hydrateDevices(const SBuffer &buf) { // Hue bulbtype - if present if (d < dev_record_len) { - zigbee_devices.setZbProfile(shortaddr, buf_d.get8(d)); + zigbee_devices.setLightProfile(shortaddr, buf_d.get8(d)); d++; } // next iteration k += dev_record_len; -//AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "Device %d After Memory = %d"), i, ESP_getFreeHeap()); } } diff --git a/tasmota/xdrv_23_zigbee_5_converters.ino b/tasmota/xdrv_23_zigbee_5_converters.ino index 6cdc245ba..60fd0dc77 100644 --- a/tasmota/xdrv_23_zigbee_5_converters.ino +++ b/tasmota/xdrv_23_zigbee_5_converters.ino @@ -91,14 +91,20 @@ bool Z_isDiscreteDataType(uint8_t t) { } typedef struct Z_AttributeConverter { - uint8_t type; - uint8_t cluster_short; - uint16_t attribute; - uint16_t name_offset; - int8_t multiplier; // multiplier for numerical value, (if > 0 multiply by x, if <0 device by x) + uint8_t type; + uint8_t cluster_short; + uint16_t attribute; + uint16_t name_offset; + int8_t multiplier; // multiplier for numerical value, (if > 0 multiply by x, if <0 device by x) + 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)) + // 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 @@ -135,413 +141,415 @@ uint8_t ClusterToCx(uint16_t cluster) { } // 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), 1 }, - { Zuint8, Cx0000, 0x0001, Z_(AppVersion), 1 }, - { Zuint8, Cx0000, 0x0002, Z_(StackVersion), 1 }, - { Zuint8, Cx0000, 0x0003, Z_(HWVersion), 1 }, - { Zstring, Cx0000, 0x0004, Z_(Manufacturer), 1 }, // record Manufacturer - { Zstring, Cx0000, 0x0005, Z_(ModelId), 1 }, // record Model - // { Zstring, Cx0000, 0x0004, Z_(Manufacturer), 1, Z_ManufKeep }, // record Manufacturer - // { Zstring, Cx0000, 0x0005, Z_(ModelId), 1, Z_ModelKeep }, // record Model - { Zstring, Cx0000, 0x0006, Z_(DateCode), 1 }, - { Zenum8, Cx0000, 0x0007, Z_(PowerSource), 1 }, - { Zenum8, Cx0000, 0x0008, Z_(GenericDeviceClass), 1 }, - { Zenum8, Cx0000, 0x0009, Z_(GenericDeviceType), 1 }, - { Zoctstr, Cx0000, 0x000A, Z_(ProductCode), 1 }, - { Zstring, Cx0000, 0x000B, Z_(ProductURL), 1 }, - { Zstring, Cx0000, 0x4000, Z_(SWBuildID), 1 }, - // { Zunk, Cx0000, 0xFFFF, nullptr, 0 }, // Remove all other values + { Zuint8, Cx0000, 0x0000, Z_(ZCLVersion), 1, 0 }, + { Zuint8, Cx0000, 0x0001, Z_(AppVersion), 1, 0 }, + { Zuint8, Cx0000, 0x0002, Z_(StackVersion), 1, 0 }, + { Zuint8, Cx0000, 0x0003, Z_(HWVersion), 1, 0 }, + { Zstring, Cx0000, 0x0004, Z_(Manufacturer), 1, 0 }, // record Manufacturer + { Zstring, Cx0000, 0x0005, Z_(ModelId), 1, 0 }, // record Model + // { Zstring, Cx0000, 0x0004, Z_(Manufacturer), 1, Z_ManufKeep, 0 }, // record Manufacturer + // { Zstring, Cx0000, 0x0005, Z_(ModelId), 1, Z_ModelKeep, 0 }, // record Model + { Zstring, Cx0000, 0x0006, Z_(DateCode), 1, 0 }, + { Zenum8, Cx0000, 0x0007, Z_(PowerSource), 1, 0 }, + { Zenum8, Cx0000, 0x0008, Z_(GenericDeviceClass), 1, 0 }, + { Zenum8, Cx0000, 0x0009, Z_(GenericDeviceType), 1, 0 }, + { Zoctstr, Cx0000, 0x000A, Z_(ProductCode), 1, 0 }, + { Zstring, Cx0000, 0x000B, Z_(ProductURL), 1, 0 }, + { Zstring, Cx0000, 0x4000, Z_(SWBuildID), 1, 0 }, + // { Zunk, Cx0000, 0xFFFF, nullptr, 0, 0 }, // Remove all other values // Cmd 0x0A - Cluster 0x0000, attribute 0xFF01 - proprietary - { Zmap8, Cx0000, 0xFF01, Z_(), 0 }, - { Zmap8, Cx0000, 0xFF02, Z_(), 0 }, - // { Zmap8, Cx0000, 0xFF01, Z_(), 0, Z_AqaraSensor }, - // { Zmap8, Cx0000, 0xFF02, Z_(), 0, Z_AqaraSensor2 }, + { Zmap8, Cx0000, 0xFF01, Z_(), 0, 0 }, + { Zmap8, Cx0000, 0xFF02, Z_(), 0, 0 }, + // { Zmap8, Cx0000, 0xFF01, Z_(), 0, Z_AqaraSensor, 0 }, + // { Zmap8, Cx0000, 0xFF02, Z_(), 0, Z_AqaraSensor2, 0 }, // Power Configuration cluster - { Zuint16, Cx0001, 0x0000, Z_(MainsVoltage), 1 }, - { Zuint8, Cx0001, 0x0001, Z_(MainsFrequency), 1 }, - { Zuint8, Cx0001, 0x0020, Z_(BatteryVoltage), -10 }, // divide by 10 - { Zuint8, Cx0001, 0x0021, Z_(BatteryPercentage), -2 }, // divide by 2 - // { Zuint8, Cx0001, 0x0021, Z_(BatteryPercentage), -2, Z_BatteryPercentage }, // divide by 2 + { Zuint16, Cx0001, 0x0000, Z_(MainsVoltage), 1, 0 }, + { Zuint8, Cx0001, 0x0001, Z_(MainsFrequency), 1, 0 }, + { Zuint8, Cx0001, 0x0020, Z_(BatteryVoltage), -10, 0 }, // divide by 10 + { Zuint8, Cx0001, 0x0021, Z_(BatteryPercentage), -2, 0 }, // divide by 2 + // { Zuint8, Cx0001, 0x0021, Z_(BatteryPercentage), -2, Z_BatteryPercentage, 0 }, // divide by 2 // Device Temperature Configuration cluster - { Zint16, Cx0002, 0x0000, Z_(CurrentTemperature), 1 }, - { Zint16, Cx0002, 0x0001, Z_(MinTempExperienced), 1 }, - { Zint16, Cx0002, 0x0002, Z_(MaxTempExperienced), 1 }, - { Zuint16, Cx0002, 0x0003, Z_(OverTempTotalDwell), 1 }, + { Zint16, Cx0002, 0x0000, Z_(CurrentTemperature), 1, 0 }, + { Zint16, Cx0002, 0x0001, Z_(MinTempExperienced), 1, 0 }, + { Zint16, Cx0002, 0x0002, Z_(MaxTempExperienced), 1, 0 }, + { Zuint16, Cx0002, 0x0003, Z_(OverTempTotalDwell), 1, 0 }, // Identify cluster - { Zuint16, Cx0003, 0x0000, Z_(IdentifyTime), 1 }, + { Zuint16, Cx0003, 0x0000, Z_(IdentifyTime), 1, 0 }, // Groups cluster - { Zmap8, Cx0004, 0x0000, Z_(GroupNameSupport), 1 }, + { Zmap8, Cx0004, 0x0000, Z_(GroupNameSupport), 1, 0 }, // Scenes cluster - { Zuint8, Cx0005, 0x0000, Z_(SceneCount), 1 }, - { Zuint8, Cx0005, 0x0001, Z_(CurrentScene), 1 }, - { Zuint16, Cx0005, 0x0002, Z_(CurrentGroup), 1 }, - { Zbool, Cx0005, 0x0003, Z_(SceneValid), 1 }, - //{ Zmap8, Cx0005, 0x0004, (NameSupport), 1 }, + { Zuint8, Cx0005, 0x0000, Z_(SceneCount), 1, 0 }, + { Zuint8, Cx0005, 0x0001, Z_(CurrentScene), 1, 0 }, + { Zuint16, Cx0005, 0x0002, Z_(CurrentGroup), 1, 0 }, + { Zbool, Cx0005, 0x0003, Z_(SceneValid), 1, 0 }, + //{ Zmap8, Cx0005, 0x0004, (NameSupport), 1, 0 }, // On/off cluster - { Zbool, Cx0006, 0x0000, Z_(Power), 1 }, - { Zenum8, Cx0006, 0x4003, Z_(StartUpOnOff), 1 }, - { Zbool, Cx0006, 0x8000, Z_(Power), 1 }, // See 7280 + { Zbool, Cx0006, 0x0000, Z_(Power), 1, 0 }, + { Zenum8, Cx0006, 0x4003, Z_(StartUpOnOff), 1, 0 }, + { Zbool, Cx0006, 0x8000, Z_(Power), 1, 0 }, // See 7280 // On/Off Switch Configuration cluster - { Zenum8, Cx0007, 0x0000, Z_(SwitchType), 1 }, + { Zenum8, Cx0007, 0x0000, Z_(SwitchType), 1, 0 }, // Level Control cluster - { Zuint8, Cx0008, 0x0000, Z_(Dimmer), 1 }, - { Zmap8, Cx0008, 0x000F, Z_(DimmerOptions), 1 }, - { Zuint16, Cx0008, 0x0001, Z_(DimmerRemainingTime), 1 }, - { Zuint16, Cx0008, 0x0010, Z_(OnOffTransitionTime), 1 }, - // { Zuint8, Cx0008, 0x0011, (OnLevel), 1 }, - // { Zuint16, Cx0008, 0x0012, (OnTransitionTime), 1 }, - // { Zuint16, Cx0008, 0x0013, (OffTransitionTime), 1 }, - // { Zuint16, Cx0008, 0x0014, (DefaultMoveRate), 1 }, + { Zuint8, Cx0008, 0x0000, Z_(Dimmer), 1, Z_MAPPING(Z_Data_Light, dimmer) }, + { Zmap8, Cx0008, 0x000F, Z_(DimmerOptions), 1, 0 }, + { Zuint16, Cx0008, 0x0001, Z_(DimmerRemainingTime), 1, 0 }, + { Zuint16, Cx0008, 0x0010, Z_(OnOffTransitionTime), 1, 0 }, + // { Zuint8, Cx0008, 0x0011, (OnLevel), 1, 0 }, + // { Zuint16, Cx0008, 0x0012, (OnTransitionTime), 1, 0 }, + // { Zuint16, Cx0008, 0x0013, (OffTransitionTime), 1, 0 }, + // { Zuint16, Cx0008, 0x0014, (DefaultMoveRate), 1, 0 }, // Alarms cluster - { Zuint16, Cx0009, 0x0000, Z_(AlarmCount), 1 }, + { Zuint16, Cx0009, 0x0000, Z_(AlarmCount), 1, 0 }, // Time cluster - { ZUTC, Cx000A, 0x0000, Z_(Time), 1 }, - { Zmap8, Cx000A, 0x0001, Z_(TimeStatus), 1 }, - { Zint32, Cx000A, 0x0002, Z_(TimeZone), 1 }, - { Zuint32, Cx000A, 0x0003, Z_(DstStart), 1 }, - { Zuint32, Cx000A, 0x0004, Z_(DstEnd), 1 }, - { Zint32, Cx000A, 0x0005, Z_(DstShift), 1 }, - { Zuint32, Cx000A, 0x0006, Z_(StandardTime), 1 }, - { Zuint32, Cx000A, 0x0007, Z_(LocalTime), 1 }, - { ZUTC, Cx000A, 0x0008, Z_(LastSetTime), 1 }, - { ZUTC, Cx000A, 0x0009, Z_(ValidUntilTime), 1 }, - { ZUTC, Cx000A, 0xFF00, Z_(TimeEpoch), 1 }, // Tasmota specific, epoch + { ZUTC, Cx000A, 0x0000, Z_(Time), 1, 0 }, + { Zmap8, Cx000A, 0x0001, Z_(TimeStatus), 1, 0 }, + { Zint32, Cx000A, 0x0002, Z_(TimeZone), 1, 0 }, + { Zuint32, Cx000A, 0x0003, Z_(DstStart), 1, 0 }, + { Zuint32, Cx000A, 0x0004, Z_(DstEnd), 1, 0 }, + { Zint32, Cx000A, 0x0005, Z_(DstShift), 1, 0 }, + { Zuint32, Cx000A, 0x0006, Z_(StandardTime), 1, 0 }, + { Zuint32, Cx000A, 0x0007, Z_(LocalTime), 1, 0 }, + { ZUTC, Cx000A, 0x0008, Z_(LastSetTime), 1, 0 }, + { ZUTC, Cx000A, 0x0009, Z_(ValidUntilTime), 1, 0 }, + { ZUTC, Cx000A, 0xFF00, Z_(TimeEpoch), 1, 0 }, // Tasmota specific, epoch // RSSI Location cluster - { Zdata8, Cx000B, 0x0000, Z_(LocationType), 1 }, - { Zenum8, Cx000B, 0x0001, Z_(LocationMethod), 1 }, - { Zuint16, Cx000B, 0x0002, Z_(LocationAge), 1 }, - { Zuint8, Cx000B, 0x0003, Z_(QualityMeasure), 1 }, - { Zuint8, Cx000B, 0x0004, Z_(NumberOfDevices), 1 }, + { Zdata8, Cx000B, 0x0000, Z_(LocationType), 1, 0 }, + { Zenum8, Cx000B, 0x0001, Z_(LocationMethod), 1, 0 }, + { Zuint16, Cx000B, 0x0002, Z_(LocationAge), 1, 0 }, + { Zuint8, Cx000B, 0x0003, Z_(QualityMeasure), 1, 0 }, + { Zuint8, Cx000B, 0x0004, Z_(NumberOfDevices), 1, 0 }, // Analog Input cluster - // { 0xFF, Cx000C, 0x0004, (AnalogInActiveText), 1 }, - { Zstring, Cx000C, 0x001C, Z_(AnalogInDescription), 1 }, - // { 0xFF, Cx000C, 0x002E, (AnalogInInactiveText), 1 }, - { Zsingle, Cx000C, 0x0041, Z_(AnalogInMaxValue), 1 }, - { Zsingle, Cx000C, 0x0045, Z_(AnalogInMinValue), 1 }, - { Zbool, Cx000C, 0x0051, Z_(AnalogInOutOfService), 1 }, - { Zsingle, Cx000C, 0x0055, Z_(AqaraRotate), 1 }, - // { 0xFF, Cx000C, 0x0057, (AnalogInPriorityArray),1 }, - { Zenum8, Cx000C, 0x0067, Z_(AnalogInReliability), 1 }, - // { 0xFF, Cx000C, 0x0068, (AnalogInRelinquishDefault),1 }, - { Zsingle, Cx000C, 0x006A, Z_(AnalogInResolution), 1 }, - { Zmap8, Cx000C, 0x006F, Z_(AnalogInStatusFlags), 1 }, - { Zenum16, Cx000C, 0x0075, Z_(AnalogInEngineeringUnits),1 }, - { Zuint32, Cx000C, 0x0100, Z_(AnalogInApplicationType),1 }, - { Zuint16, Cx000C, 0xFF05, Z_(Aqara_FF05), 1 }, + // { 0xFF, Cx000C, 0x0004, (AnalogInActiveText), 1, 0 }, + { Zstring, Cx000C, 0x001C, Z_(AnalogInDescription), 1, 0 }, + // { 0xFF, Cx000C, 0x002E, (AnalogInInactiveText), 1, 0 }, + { Zsingle, Cx000C, 0x0041, Z_(AnalogInMaxValue), 1, 0 }, + { Zsingle, Cx000C, 0x0045, Z_(AnalogInMinValue), 1, 0 }, + { Zbool, Cx000C, 0x0051, Z_(AnalogInOutOfService), 1, 0 }, + { Zsingle, Cx000C, 0x0055, Z_(AqaraRotate), 1, 0 }, + // { 0xFF, Cx000C, 0x0057, (AnalogInPriorityArray),1, 0 }, + { Zenum8, Cx000C, 0x0067, Z_(AnalogInReliability), 1, 0 }, + // { 0xFF, Cx000C, 0x0068, (AnalogInRelinquishDefault),1, 0 }, + { Zsingle, Cx000C, 0x006A, Z_(AnalogInResolution), 1, 0 }, + { Zmap8, Cx000C, 0x006F, Z_(AnalogInStatusFlags), 1, 0 }, + { Zenum16, Cx000C, 0x0075, Z_(AnalogInEngineeringUnits),1, 0 }, + { Zuint32, Cx000C, 0x0100, Z_(AnalogInApplicationType),1, 0 }, + { Zuint16, Cx000C, 0xFF05, Z_(Aqara_FF05), 1, 0 }, // Analog Output cluster - { Zstring, Cx000D, 0x001C, Z_(AnalogOutDescription), 1 }, - { Zsingle, Cx000D, 0x0041, Z_(AnalogOutMaxValue), 1 }, - { Zsingle, Cx000D, 0x0045, Z_(AnalogOutMinValue), 1 }, - { Zbool, Cx000D, 0x0051, Z_(AnalogOutOutOfService),1 }, - { Zsingle, Cx000D, 0x0055, Z_(AnalogOutValue), 1 }, - // { Zunk, Cx000D, 0x0057, (AnalogOutPriorityArray),1 }, - { Zenum8, Cx000D, 0x0067, Z_(AnalogOutReliability), 1 }, - { Zsingle, Cx000D, 0x0068, Z_(AnalogOutRelinquishDefault),1 }, - { Zsingle, Cx000D, 0x006A, Z_(AnalogOutResolution), 1 }, - { Zmap8, Cx000D, 0x006F, Z_(AnalogOutStatusFlags), 1 }, - { Zenum16, Cx000D, 0x0075, Z_(AnalogOutEngineeringUnits),1 }, - { Zuint32, Cx000D, 0x0100, Z_(AnalogOutApplicationType),1 }, + { Zstring, Cx000D, 0x001C, Z_(AnalogOutDescription), 1, 0 }, + { Zsingle, Cx000D, 0x0041, Z_(AnalogOutMaxValue), 1, 0 }, + { Zsingle, Cx000D, 0x0045, Z_(AnalogOutMinValue), 1, 0 }, + { Zbool, Cx000D, 0x0051, Z_(AnalogOutOutOfService),1, 0 }, + { Zsingle, Cx000D, 0x0055, Z_(AnalogOutValue), 1, 0 }, + // { Zunk, Cx000D, 0x0057, (AnalogOutPriorityArray),1, 0 }, + { Zenum8, Cx000D, 0x0067, Z_(AnalogOutReliability), 1, 0 }, + { Zsingle, Cx000D, 0x0068, Z_(AnalogOutRelinquishDefault),1, 0 }, + { Zsingle, Cx000D, 0x006A, Z_(AnalogOutResolution), 1, 0 }, + { Zmap8, Cx000D, 0x006F, Z_(AnalogOutStatusFlags), 1, 0 }, + { Zenum16, Cx000D, 0x0075, Z_(AnalogOutEngineeringUnits),1, 0 }, + { Zuint32, Cx000D, 0x0100, Z_(AnalogOutApplicationType),1, 0 }, // Analog Value cluster - { Zstring, Cx000E, 0x001C, Z_(AnalogDescription), 1 }, - { Zbool, Cx000E, 0x0051, Z_(AnalogOutOfService), 1 }, - { Zsingle, Cx000E, 0x0055, Z_(AnalogValue), 1 }, - { Zunk, Cx000E, 0x0057, Z_(AnalogPriorityArray), 1 }, - { Zenum8, Cx000E, 0x0067, Z_(AnalogReliability), 1 }, - { Zsingle, Cx000E, 0x0068, Z_(AnalogRelinquishDefault),1 }, - { Zmap8, Cx000E, 0x006F, Z_(AnalogStatusFlags), 1 }, - { Zenum16, Cx000E, 0x0075, Z_(AnalogEngineeringUnits),1 }, - { Zuint32, Cx000E, 0x0100, Z_(AnalogApplicationType),1 }, + { Zstring, Cx000E, 0x001C, Z_(AnalogDescription), 1, 0 }, + { Zbool, Cx000E, 0x0051, Z_(AnalogOutOfService), 1, 0 }, + { Zsingle, Cx000E, 0x0055, Z_(AnalogValue), 1, 0 }, + { Zunk, Cx000E, 0x0057, Z_(AnalogPriorityArray), 1, 0 }, + { Zenum8, Cx000E, 0x0067, Z_(AnalogReliability), 1, 0 }, + { Zsingle, Cx000E, 0x0068, Z_(AnalogRelinquishDefault),1, 0 }, + { Zmap8, Cx000E, 0x006F, Z_(AnalogStatusFlags), 1, 0 }, + { Zenum16, Cx000E, 0x0075, Z_(AnalogEngineeringUnits),1, 0 }, + { Zuint32, Cx000E, 0x0100, Z_(AnalogApplicationType),1, 0 }, // Binary Input cluster - { Zstring, Cx000F, 0x0004, Z_(BinaryInActiveText), 1 }, - { Zstring, Cx000F, 0x001C, Z_(BinaryInDescription), 1 }, - { Zstring, Cx000F, 0x002E, Z_(BinaryInInactiveText),1 }, - { Zbool, Cx000F, 0x0051, Z_(BinaryInOutOfService),1 }, - { Zenum8, Cx000F, 0x0054, Z_(BinaryInPolarity), 1 }, - { Zstring, Cx000F, 0x0055, Z_(BinaryInValue), 1 }, - // { 0xFF, Cx000F, 0x0057, (BinaryInPriorityArray),1 }, - { Zenum8, Cx000F, 0x0067, Z_(BinaryInReliability), 1 }, - { Zmap8, Cx000F, 0x006F, Z_(BinaryInStatusFlags), 1 }, - { Zuint32, Cx000F, 0x0100, Z_(BinaryInApplicationType),1 }, + { Zstring, Cx000F, 0x0004, Z_(BinaryInActiveText), 1, 0 }, + { Zstring, Cx000F, 0x001C, Z_(BinaryInDescription), 1, 0 }, + { Zstring, Cx000F, 0x002E, Z_(BinaryInInactiveText),1, 0 }, + { Zbool, Cx000F, 0x0051, Z_(BinaryInOutOfService),1, 0 }, + { Zenum8, Cx000F, 0x0054, Z_(BinaryInPolarity), 1, 0 }, + { Zstring, Cx000F, 0x0055, Z_(BinaryInValue), 1, 0 }, + // { 0xFF, Cx000F, 0x0057, (BinaryInPriorityArray),1, 0 }, + { Zenum8, Cx000F, 0x0067, Z_(BinaryInReliability), 1, 0 }, + { Zmap8, Cx000F, 0x006F, Z_(BinaryInStatusFlags), 1, 0 }, + { Zuint32, Cx000F, 0x0100, Z_(BinaryInApplicationType),1, 0 }, // Binary Output cluster - { Zstring, Cx0010, 0x0004, Z_(BinaryOutActiveText), 1 }, - { Zstring, Cx0010, 0x001C, Z_(BinaryOutDescription), 1 }, - { Zstring, Cx0010, 0x002E, Z_(BinaryOutInactiveText),1 }, - { Zuint32, Cx0010, 0x0042, Z_(BinaryOutMinimumOffTime),1 }, - { Zuint32, Cx0010, 0x0043, Z_(BinaryOutMinimumOnTime),1 }, - { Zbool, Cx0010, 0x0051, Z_(BinaryOutOutOfService),1 }, - { Zenum8, Cx0010, 0x0054, Z_(BinaryOutPolarity), 1 }, - { Zbool, Cx0010, 0x0055, Z_(BinaryOutValue), 1 }, - // { Zunk, Cx0010, 0x0057, (BinaryOutPriorityArray),1 }, - { Zenum8, Cx0010, 0x0067, Z_(BinaryOutReliability), 1 }, - { Zbool, Cx0010, 0x0068, Z_(BinaryOutRelinquishDefault),1 }, - { Zmap8, Cx0010, 0x006F, Z_(BinaryOutStatusFlags), 1 }, - { Zuint32, Cx0010, 0x0100, Z_(BinaryOutApplicationType),1 }, + { Zstring, Cx0010, 0x0004, Z_(BinaryOutActiveText), 1, 0 }, + { Zstring, Cx0010, 0x001C, Z_(BinaryOutDescription), 1, 0 }, + { Zstring, Cx0010, 0x002E, Z_(BinaryOutInactiveText),1, 0 }, + { Zuint32, Cx0010, 0x0042, Z_(BinaryOutMinimumOffTime),1, 0 }, + { Zuint32, Cx0010, 0x0043, Z_(BinaryOutMinimumOnTime),1, 0 }, + { Zbool, Cx0010, 0x0051, Z_(BinaryOutOutOfService),1, 0 }, + { Zenum8, Cx0010, 0x0054, Z_(BinaryOutPolarity), 1, 0 }, + { Zbool, Cx0010, 0x0055, Z_(BinaryOutValue), 1, 0 }, + // { Zunk, Cx0010, 0x0057, (BinaryOutPriorityArray),1, 0 }, + { Zenum8, Cx0010, 0x0067, Z_(BinaryOutReliability), 1, 0 }, + { Zbool, Cx0010, 0x0068, Z_(BinaryOutRelinquishDefault),1, 0 }, + { Zmap8, Cx0010, 0x006F, Z_(BinaryOutStatusFlags), 1, 0 }, + { Zuint32, Cx0010, 0x0100, Z_(BinaryOutApplicationType),1, 0 }, // Binary Value cluster - { Zstring, Cx0011, 0x0004, Z_(BinaryActiveText), 1 }, - { Zstring, Cx0011, 0x001C, Z_(BinaryDescription), 1 }, - { Zstring, Cx0011, 0x002E, Z_(BinaryInactiveText), 1 }, - { Zuint32, Cx0011, 0x0042, Z_(BinaryMinimumOffTime), 1 }, - { Zuint32, Cx0011, 0x0043, Z_(BinaryMinimumOnTime), 1 }, - { Zbool, Cx0011, 0x0051, Z_(BinaryOutOfService), 1 }, - { Zbool, Cx0011, 0x0055, Z_(BinaryValue), 1 }, - // { Zunk, Cx0011, 0x0057, (BinaryPriorityArray), 1 }, - { Zenum8, Cx0011, 0x0067, Z_(BinaryReliability), 1 }, - { Zbool, Cx0011, 0x0068, Z_(BinaryRelinquishDefault),1 }, - { Zmap8, Cx0011, 0x006F, Z_(BinaryStatusFlags), 1 }, - { Zuint32, Cx0011, 0x0100, Z_(BinaryApplicationType),1 }, + { Zstring, Cx0011, 0x0004, Z_(BinaryActiveText), 1, 0 }, + { Zstring, Cx0011, 0x001C, Z_(BinaryDescription), 1, 0 }, + { Zstring, Cx0011, 0x002E, Z_(BinaryInactiveText), 1, 0 }, + { Zuint32, Cx0011, 0x0042, Z_(BinaryMinimumOffTime), 1, 0 }, + { Zuint32, Cx0011, 0x0043, Z_(BinaryMinimumOnTime), 1, 0 }, + { Zbool, Cx0011, 0x0051, Z_(BinaryOutOfService), 1, 0 }, + { Zbool, Cx0011, 0x0055, Z_(BinaryValue), 1, 0 }, + // { Zunk, Cx0011, 0x0057, (BinaryPriorityArray), 1, 0 }, + { Zenum8, Cx0011, 0x0067, Z_(BinaryReliability), 1, 0 }, + { Zbool, Cx0011, 0x0068, Z_(BinaryRelinquishDefault),1, 0 }, + { Zmap8, Cx0011, 0x006F, Z_(BinaryStatusFlags), 1, 0 }, + { Zuint32, Cx0011, 0x0100, Z_(BinaryApplicationType),1, 0 }, // Multistate Input cluster - // { Zunk, Cx0012, 0x000E, (MultiInStateText), 1 }, - { Zstring, Cx0012, 0x001C, Z_(MultiInDescription), 1 }, - { Zuint16, Cx0012, 0x004A, Z_(MultiInNumberOfStates),1 }, - { Zbool, Cx0012, 0x0051, Z_(MultiInOutOfService), 1 }, - { Zuint16, Cx0012, 0x0055, Z_(MultiInValue), 1 }, - // { Zuint16, Cx0012, 0x0055, Z_(MultiInValue), 0, Z_AqaraCube }, - // { Zuint16, Cx0012, 0x0055, Z_(MultiInValue), 0, Z_AqaraButton }, - { Zenum8, Cx0012, 0x0067, Z_(MultiInReliability), 1 }, - { Zmap8, Cx0012, 0x006F, Z_(MultiInStatusFlags), 1 }, - { Zuint32, Cx0012, 0x0100, Z_(MultiInApplicationType),1 }, + // { Zunk, Cx0012, 0x000E, (MultiInStateText), 1, 0 }, + { Zstring, Cx0012, 0x001C, Z_(MultiInDescription), 1, 0 }, + { Zuint16, Cx0012, 0x004A, Z_(MultiInNumberOfStates),1, 0 }, + { Zbool, Cx0012, 0x0051, Z_(MultiInOutOfService), 1, 0 }, + { Zuint16, Cx0012, 0x0055, Z_(MultiInValue), 1, 0 }, + // { Zuint16, Cx0012, 0x0055, Z_(MultiInValue), 0, Z_AqaraCube, 0 }, + // { Zuint16, Cx0012, 0x0055, Z_(MultiInValue), 0, Z_AqaraButton, 0 }, + { Zenum8, Cx0012, 0x0067, Z_(MultiInReliability), 1, 0 }, + { Zmap8, Cx0012, 0x006F, Z_(MultiInStatusFlags), 1, 0 }, + { Zuint32, Cx0012, 0x0100, Z_(MultiInApplicationType),1, 0 }, // Multistate output - // { Zunk, Cx0013, 0x000E, (MultiOutStateText), 1 }, - { Zstring, Cx0013, 0x001C, Z_(MultiOutDescription), 1 }, - { Zuint16, Cx0013, 0x004A, Z_(MultiOutNumberOfStates),1 }, - { Zbool, Cx0013, 0x0051, Z_(MultiOutOutOfService), 1 }, - { Zuint16, Cx0013, 0x0055, Z_(MultiOutValue), 1 }, - // { Zunk, Cx0013, 0x0057, (MultiOutPriorityArray),1 }, - { Zenum8, Cx0013, 0x0067, Z_(MultiOutReliability), 1 }, - { Zuint16, Cx0013, 0x0068, Z_(MultiOutRelinquishDefault),1 }, - { Zmap8, Cx0013, 0x006F, Z_(MultiOutStatusFlags), 1 }, - { Zuint32, Cx0013, 0x0100, Z_(MultiOutApplicationType),1 }, + // { Zunk, Cx0013, 0x000E, (MultiOutStateText), 1, 0 }, + { Zstring, Cx0013, 0x001C, Z_(MultiOutDescription), 1, 0 }, + { Zuint16, Cx0013, 0x004A, Z_(MultiOutNumberOfStates),1, 0 }, + { Zbool, Cx0013, 0x0051, Z_(MultiOutOutOfService), 1, 0 }, + { Zuint16, Cx0013, 0x0055, Z_(MultiOutValue), 1, 0 }, + // { Zunk, Cx0013, 0x0057, (MultiOutPriorityArray),1, 0 }, + { Zenum8, Cx0013, 0x0067, Z_(MultiOutReliability), 1, 0 }, + { Zuint16, Cx0013, 0x0068, Z_(MultiOutRelinquishDefault),1, 0 }, + { Zmap8, Cx0013, 0x006F, Z_(MultiOutStatusFlags), 1, 0 }, + { Zuint32, Cx0013, 0x0100, Z_(MultiOutApplicationType),1, 0 }, // Multistate Value cluster - // { Zunk, Cx0014, 0x000E, (MultiStateText), 1 }, - { Zstring, Cx0014, 0x001C, Z_(MultiDescription), 1 }, - { Zuint16, Cx0014, 0x004A, Z_(MultiNumberOfStates), 1 }, - { Zbool, Cx0014, 0x0051, Z_(MultiOutOfService), 1 }, - { Zuint16, Cx0014, 0x0055, Z_(MultiValue), 1 }, - { Zenum8, Cx0014, 0x0067, Z_(MultiReliability), 1 }, - { Zuint16, Cx0014, 0x0068, Z_(MultiRelinquishDefault),1 }, - { Zmap8, Cx0014, 0x006F, Z_(MultiStatusFlags), 1 }, - { Zuint32, Cx0014, 0x0100, Z_(MultiApplicationType), 1 }, + // { Zunk, Cx0014, 0x000E, (MultiStateText), 1, 0 }, + { Zstring, Cx0014, 0x001C, Z_(MultiDescription), 1, 0 }, + { Zuint16, Cx0014, 0x004A, Z_(MultiNumberOfStates), 1, 0 }, + { Zbool, Cx0014, 0x0051, Z_(MultiOutOfService), 1, 0 }, + { Zuint16, Cx0014, 0x0055, Z_(MultiValue), 1, 0 }, + { Zenum8, Cx0014, 0x0067, Z_(MultiReliability), 1, 0 }, + { Zuint16, Cx0014, 0x0068, Z_(MultiRelinquishDefault),1, 0 }, + { Zmap8, Cx0014, 0x006F, Z_(MultiStatusFlags), 1, 0 }, + { Zuint32, Cx0014, 0x0100, Z_(MultiApplicationType), 1, 0 }, // Power Profile cluster - { Zuint8, Cx001A, 0x0000, Z_(TotalProfileNum), 1 }, - { Zbool, Cx001A, 0x0001, Z_(MultipleScheduling), 1 }, - { Zmap8, Cx001A, 0x0002, Z_(EnergyFormatting), 1 }, - { Zbool, Cx001A, 0x0003, Z_(EnergyRemote), 1 }, - { Zmap8, Cx001A, 0x0004, Z_(ScheduleMode), 1 }, + { Zuint8, Cx001A, 0x0000, Z_(TotalProfileNum), 1, 0 }, + { Zbool, Cx001A, 0x0001, Z_(MultipleScheduling), 1, 0 }, + { Zmap8, Cx001A, 0x0002, Z_(EnergyFormatting), 1, 0 }, + { Zbool, Cx001A, 0x0003, Z_(EnergyRemote), 1, 0 }, + { Zmap8, Cx001A, 0x0004, Z_(ScheduleMode), 1, 0 }, // Poll Control cluster - { Zuint32, Cx0020, 0x0000, Z_(CheckinInterval), 1 }, - { Zuint32, Cx0020, 0x0001, Z_(LongPollInterval), 1 }, - { Zuint16, Cx0020, 0x0002, Z_(ShortPollInterval), 1 }, - { Zuint16, Cx0020, 0x0003, Z_(FastPollTimeout), 1 }, - { Zuint32, Cx0020, 0x0004, Z_(CheckinIntervalMin), 1 }, - { Zuint32, Cx0020, 0x0005, Z_(LongPollIntervalMin), 1 }, - { Zuint16, Cx0020, 0x0006, Z_(FastPollTimeoutMax), 1 }, + { Zuint32, Cx0020, 0x0000, Z_(CheckinInterval), 1, 0 }, + { Zuint32, Cx0020, 0x0001, Z_(LongPollInterval), 1, 0 }, + { Zuint16, Cx0020, 0x0002, Z_(ShortPollInterval), 1, 0 }, + { Zuint16, Cx0020, 0x0003, Z_(FastPollTimeout), 1, 0 }, + { Zuint32, Cx0020, 0x0004, Z_(CheckinIntervalMin), 1, 0 }, + { Zuint32, Cx0020, 0x0005, Z_(LongPollIntervalMin), 1, 0 }, + { Zuint16, Cx0020, 0x0006, Z_(FastPollTimeoutMax), 1, 0 }, // Shade Configuration cluster - { Zuint16, Cx0100, 0x0000, Z_(PhysicalClosedLimit), 1 }, - { Zuint8, Cx0100, 0x0001, Z_(MotorStepSize), 1 }, - { Zmap8, Cx0100, 0x0002, Z_(Status), 1 }, - { Zuint16, Cx0100, 0x0010, Z_(ClosedLimit), 1 }, - { Zenum8, Cx0100, 0x0011, Z_(Mode), 1 }, + { Zuint16, Cx0100, 0x0000, Z_(PhysicalClosedLimit), 1, 0 }, + { Zuint8, Cx0100, 0x0001, Z_(MotorStepSize), 1, 0 }, + { Zmap8, Cx0100, 0x0002, Z_(Status), 1, 0 }, + { Zuint16, Cx0100, 0x0010, Z_(ClosedLimit), 1, 0 }, + { Zenum8, Cx0100, 0x0011, Z_(Mode), 1, 0 }, // Door Lock cluster - { Zenum8, Cx0101, 0x0000, Z_(LockState), 1 }, - { Zenum8, Cx0101, 0x0001, Z_(LockType), 1 }, - { Zbool, Cx0101, 0x0002, Z_(ActuatorEnabled), 1 }, - { Zenum8, Cx0101, 0x0003, Z_(DoorState), 1 }, - { Zuint32, Cx0101, 0x0004, Z_(DoorOpenEvents), 1 }, - { Zuint32, Cx0101, 0x0005, Z_(DoorClosedEvents), 1 }, - { Zuint16, Cx0101, 0x0006, Z_(OpenPeriod), 1 }, + { Zenum8, Cx0101, 0x0000, Z_(LockState), 1, 0 }, + { Zenum8, Cx0101, 0x0001, Z_(LockType), 1, 0 }, + { Zbool, Cx0101, 0x0002, Z_(ActuatorEnabled), 1, 0 }, + { Zenum8, Cx0101, 0x0003, Z_(DoorState), 1, 0 }, + { Zuint32, Cx0101, 0x0004, Z_(DoorOpenEvents), 1, 0 }, + { Zuint32, Cx0101, 0x0005, Z_(DoorClosedEvents), 1, 0 }, + { Zuint16, Cx0101, 0x0006, Z_(OpenPeriod), 1, 0 }, // Aqara Lumi Vibration Sensor - { Zuint16, Cx0101, 0x0055, Z_(AqaraVibrationMode), 1 }, - { Zuint16, Cx0101, 0x0503, Z_(AqaraVibrationsOrAngle), 1 }, - { Zuint32, Cx0101, 0x0505, Z_(AqaraVibration505), 1 }, - { Zuint48, Cx0101, 0x0508, Z_(AqaraAccelerometer), 1 }, + { Zuint16, Cx0101, 0x0055, Z_(AqaraVibrationMode), 1, 0 }, + { Zuint16, Cx0101, 0x0503, Z_(AqaraVibrationsOrAngle), 1, 0 }, + { Zuint32, Cx0101, 0x0505, Z_(AqaraVibration505), 1, 0 }, + { Zuint48, Cx0101, 0x0508, Z_(AqaraAccelerometer), 1, 0 }, // Window Covering cluster - { Zenum8, Cx0102, 0x0000, Z_(WindowCoveringType), 1 }, - { Zuint16, Cx0102, 0x0001, Z_(PhysicalClosedLimitLift),1 }, - { Zuint16, Cx0102, 0x0002, Z_(PhysicalClosedLimitTilt),1 }, - { Zuint16, Cx0102, 0x0003, Z_(CurrentPositionLift), 1 }, - { Zuint16, Cx0102, 0x0004, Z_(CurrentPositionTilt), 1 }, - { Zuint16, Cx0102, 0x0005, Z_(NumberofActuationsLift),1 }, - { Zuint16, Cx0102, 0x0006, Z_(NumberofActuationsTilt),1 }, - { Zmap8, Cx0102, 0x0007, Z_(ConfigStatus), 1 }, - { Zuint8, Cx0102, 0x0008, Z_(CurrentPositionLiftPercentage),1 }, - { Zuint8, Cx0102, 0x0009, Z_(CurrentPositionTiltPercentage),1 }, - { Zuint16, Cx0102, 0x0010, Z_(InstalledOpenLimitLift),1 }, - { Zuint16, Cx0102, 0x0011, Z_(InstalledClosedLimitLift),1 }, - { Zuint16, Cx0102, 0x0012, Z_(InstalledOpenLimitTilt),1 }, - { Zuint16, Cx0102, 0x0013, Z_(InstalledClosedLimitTilt),1 }, - { Zuint16, Cx0102, 0x0014, Z_(VelocityLift), 1 }, - { Zuint16, Cx0102, 0x0015, Z_(AccelerationTimeLift),1 }, - { Zuint16, Cx0102, 0x0016, Z_(DecelerationTimeLift), 1 }, - { Zmap8, Cx0102, 0x0017, Z_(Mode), 1 }, - { Zoctstr, Cx0102, 0x0018, Z_(IntermediateSetpointsLift),1 }, - { Zoctstr, Cx0102, 0x0019, Z_(IntermediateSetpointsTilt),1 }, + { Zenum8, Cx0102, 0x0000, Z_(WindowCoveringType), 1, 0 }, + { Zuint16, Cx0102, 0x0001, Z_(PhysicalClosedLimitLift),1, 0 }, + { Zuint16, Cx0102, 0x0002, Z_(PhysicalClosedLimitTilt),1, 0 }, + { Zuint16, Cx0102, 0x0003, Z_(CurrentPositionLift), 1, 0 }, + { Zuint16, Cx0102, 0x0004, Z_(CurrentPositionTilt), 1, 0 }, + { Zuint16, Cx0102, 0x0005, Z_(NumberofActuationsLift),1, 0 }, + { Zuint16, Cx0102, 0x0006, Z_(NumberofActuationsTilt),1, 0 }, + { Zmap8, Cx0102, 0x0007, Z_(ConfigStatus), 1, 0 }, + { Zuint8, Cx0102, 0x0008, Z_(CurrentPositionLiftPercentage),1, 0 }, + { Zuint8, Cx0102, 0x0009, Z_(CurrentPositionTiltPercentage),1, 0 }, + { Zuint16, Cx0102, 0x0010, Z_(InstalledOpenLimitLift),1, 0 }, + { Zuint16, Cx0102, 0x0011, Z_(InstalledClosedLimitLift),1, 0 }, + { Zuint16, Cx0102, 0x0012, Z_(InstalledOpenLimitTilt),1, 0 }, + { Zuint16, Cx0102, 0x0013, Z_(InstalledClosedLimitTilt),1, 0 }, + { Zuint16, Cx0102, 0x0014, Z_(VelocityLift), 1, 0 }, + { Zuint16, Cx0102, 0x0015, Z_(AccelerationTimeLift),1, 0 }, + { Zuint16, Cx0102, 0x0016, Z_(DecelerationTimeLift), 1, 0 }, + { Zmap8, Cx0102, 0x0017, Z_(Mode), 1, 0 }, + { Zoctstr, Cx0102, 0x0018, Z_(IntermediateSetpointsLift),1, 0 }, + { Zoctstr, Cx0102, 0x0019, Z_(IntermediateSetpointsTilt),1, 0 }, // Thermostat - { Zint16, Cx0201, 0x0000, Z_(LocalTemperature), -100 }, - { Zint16, Cx0201, 0x0001, Z_(OutdoorTemperature),-100 }, - { Zuint8, Cx0201, 0x0007, Z_(PICoolingDemand), 1 }, - { Zuint8, Cx0201, 0x0008, Z_(PIHeatingDemand), 1 }, - { Zint8, Cx0201, 0x0010, Z_(LocalTemperatureCalibration), -10 }, - { Zint16, Cx0201, 0x0011, Z_(OccupiedCoolingSetpoint), -100 }, - { Zint16, Cx0201, 0x0012, Z_(OccupiedHeatingSetpoint), -100 }, - { Zint16, Cx0201, 0x0013, Z_(UnoccupiedCoolingSetpoint), -100 }, - { Zint16, Cx0201, 0x0014, Z_(UnoccupiedHeatingSetpoint), -100 }, - { Zmap8, Cx0201, 0x001A, Z_(RemoteSensing), 1 }, - { Zenum8, Cx0201, 0x001B, Z_(ControlSequenceOfOperation), 1 }, - { Zenum8, Cx0201, 0x001C, Z_(SystemMode), 1 }, + { Zint16, Cx0201, 0x0000, Z_(LocalTemperature), -100, Z_MAPPING(Z_Data_Thermo, temperature) }, + { Zint16, Cx0201, 0x0001, Z_(OutdoorTemperature),-100, 0 }, + { Zuint8, Cx0201, 0x0007, Z_(PICoolingDemand), 1, Z_MAPPING(Z_Data_Thermo, th_setpoint) }, + { Zuint8, Cx0201, 0x0008, Z_(PIHeatingDemand), 1, Z_MAPPING(Z_Data_Thermo, th_setpoint) }, + { Zint8, Cx0201, 0x0010, Z_(LocalTemperatureCalibration), -10, 0 }, + { Zint16, Cx0201, 0x0011, Z_(OccupiedCoolingSetpoint), -100, Z_MAPPING(Z_Data_Thermo, temperature_target) }, + { Zint16, Cx0201, 0x0012, Z_(OccupiedHeatingSetpoint), -100, Z_MAPPING(Z_Data_Thermo, temperature_target) }, + { Zint16, Cx0201, 0x0013, Z_(UnoccupiedCoolingSetpoint), -100, 0 }, + { Zint16, Cx0201, 0x0014, Z_(UnoccupiedHeatingSetpoint), -100, 0 }, + { Zmap8, Cx0201, 0x001A, Z_(RemoteSensing), 1, 0 }, + { Zenum8, Cx0201, 0x001B, Z_(ControlSequenceOfOperation), 1, 0 }, + { Zenum8, Cx0201, 0x001C, Z_(SystemMode), 1, 0 }, // below is Eurotronic specific - { Zenum8, Cx0201, 0x4000, Z_(TRVMode), 1 }, - { Zuint8, Cx0201, 0x4001, Z_(SetValvePosition), 1 }, - { Zuint8, Cx0201, 0x4002, Z_(EurotronicErrors), 1 }, - { Zint16, Cx0201, 0x4003, Z_(CurrentTemperatureSetPoint), -100 }, + { Zenum8, Cx0201, 0x4000, Z_(TRVMode), 1, 0 }, + { Zuint8, Cx0201, 0x4001, Z_(SetValvePosition), 1, 0 }, + { Zuint8, Cx0201, 0x4002, Z_(EurotronicErrors), 1, 0 }, + { Zint16, Cx0201, 0x4003, Z_(CurrentTemperatureSetPoint), -100, 0 }, // Color Control cluster - { Zuint8, Cx0300, 0x0000, Z_(Hue), 1 }, - { Zuint8, Cx0300, 0x0001, Z_(Sat), 1 }, - { Zuint16, Cx0300, 0x0002, Z_(RemainingTime), 1 }, - { Zuint16, Cx0300, 0x0003, Z_(X), 1 }, - { Zuint16, Cx0300, 0x0004, Z_(Y), 1 }, - { Zenum8, Cx0300, 0x0005, Z_(DriftCompensation), 1 }, - { Zstring, Cx0300, 0x0006, Z_(CompensationText), 1 }, - { Zuint16, Cx0300, 0x0007, Z_(CT), 1 }, - { Zenum8, Cx0300, 0x0008, Z_(ColorMode), 1 }, - { Zuint8, Cx0300, 0x0010, Z_(NumberOfPrimaries), 1 }, - { Zuint16, Cx0300, 0x0011, Z_(Primary1X), 1 }, - { Zuint16, Cx0300, 0x0012, Z_(Primary1Y), 1 }, - { Zuint8, Cx0300, 0x0013, Z_(Primary1Intensity), 1 }, - { Zuint16, Cx0300, 0x0015, Z_(Primary2X), 1 }, - { Zuint16, Cx0300, 0x0016, Z_(Primary2Y), 1 }, - { Zuint8, Cx0300, 0x0017, Z_(Primary2Intensity), 1 }, - { Zuint16, Cx0300, 0x0019, Z_(Primary3X), 1 }, - { Zuint16, Cx0300, 0x001A, Z_(Primary3Y), 1 }, - { Zuint8, Cx0300, 0x001B, Z_(Primary3Intensity), 1 }, - { Zuint16, Cx0300, 0x0030, Z_(WhitePointX), 1 }, - { Zuint16, Cx0300, 0x0031, Z_(WhitePointY), 1 }, - { Zuint16, Cx0300, 0x0032, Z_(ColorPointRX), 1 }, - { Zuint16, Cx0300, 0x0033, Z_(ColorPointRY), 1 }, - { Zuint8, Cx0300, 0x0034, Z_(ColorPointRIntensity), 1 }, - { Zuint16, Cx0300, 0x0036, Z_(ColorPointGX), 1 }, - { Zuint16, Cx0300, 0x0037, Z_(ColorPointGY), 1 }, - { Zuint8, Cx0300, 0x0038, Z_(ColorPointGIntensity), 1 }, - { Zuint16, Cx0300, 0x003A, Z_(ColorPointBX), 1 }, - { Zuint16, Cx0300, 0x003B, Z_(ColorPointBY), 1 }, - { Zuint8, Cx0300, 0x003C, Z_(ColorPointBIntensity), 1 }, + { Zuint8, Cx0300, 0x0000, Z_(Hue), 1, Z_MAPPING(Z_Data_Light, hue) }, + { Zuint8, Cx0300, 0x0001, Z_(Sat), 1, Z_MAPPING(Z_Data_Light, sat) }, + { Zuint16, Cx0300, 0x0002, Z_(RemainingTime), 1, 0 }, + { Zuint16, Cx0300, 0x0003, Z_(X), 1, Z_MAPPING(Z_Data_Light, x) }, + { Zuint16, Cx0300, 0x0004, Z_(Y), 1, Z_MAPPING(Z_Data_Light, y) }, + { Zenum8, Cx0300, 0x0005, Z_(DriftCompensation), 1, 0 }, + { Zstring, Cx0300, 0x0006, Z_(CompensationText), 1, 0 }, + { Zuint16, Cx0300, 0x0007, Z_(CT), 1, Z_MAPPING(Z_Data_Light, ct) }, + { Zenum8, Cx0300, 0x0008, Z_(ColorMode), 1, Z_MAPPING(Z_Data_Light, colormode) }, + { Zuint8, Cx0300, 0x0010, Z_(NumberOfPrimaries), 1, 0 }, + { Zuint16, Cx0300, 0x0011, Z_(Primary1X), 1, 0 }, + { Zuint16, Cx0300, 0x0012, Z_(Primary1Y), 1, 0 }, + { Zuint8, Cx0300, 0x0013, Z_(Primary1Intensity), 1, 0 }, + { Zuint16, Cx0300, 0x0015, Z_(Primary2X), 1, 0 }, + { Zuint16, Cx0300, 0x0016, Z_(Primary2Y), 1, 0 }, + { Zuint8, Cx0300, 0x0017, Z_(Primary2Intensity), 1, 0 }, + { Zuint16, Cx0300, 0x0019, Z_(Primary3X), 1, 0 }, + { Zuint16, Cx0300, 0x001A, Z_(Primary3Y), 1, 0 }, + { Zuint8, Cx0300, 0x001B, Z_(Primary3Intensity), 1, 0 }, + { Zuint16, Cx0300, 0x0030, Z_(WhitePointX), 1, 0 }, + { Zuint16, Cx0300, 0x0031, Z_(WhitePointY), 1, 0 }, + { Zuint16, Cx0300, 0x0032, Z_(ColorPointRX), 1, 0 }, + { Zuint16, Cx0300, 0x0033, Z_(ColorPointRY), 1, 0 }, + { Zuint8, Cx0300, 0x0034, Z_(ColorPointRIntensity), 1, 0 }, + { Zuint16, Cx0300, 0x0036, Z_(ColorPointGX), 1, 0 }, + { Zuint16, Cx0300, 0x0037, Z_(ColorPointGY), 1, 0 }, + { Zuint8, Cx0300, 0x0038, Z_(ColorPointGIntensity), 1, 0 }, + { Zuint16, Cx0300, 0x003A, Z_(ColorPointBX), 1, 0 }, + { Zuint16, Cx0300, 0x003B, Z_(ColorPointBY), 1, 0 }, + { Zuint8, Cx0300, 0x003C, Z_(ColorPointBIntensity), 1, 0 }, // Illuminance Measurement cluster - { Zuint16, Cx0400, 0x0000, Z_(Illuminance), 1 }, // Illuminance (in Lux) - { Zuint16, Cx0400, 0x0001, Z_(IlluminanceMinMeasuredValue), 1 }, // - { Zuint16, Cx0400, 0x0002, Z_(IlluminanceMaxMeasuredValue), 1 }, // - { Zuint16, Cx0400, 0x0003, Z_(IlluminanceTolerance), 1 }, // - { Zenum8, Cx0400, 0x0004, Z_(IlluminanceLightSensorType), 1 }, // - { Zunk, Cx0400, 0xFFFF, Z_(), 0 }, // Remove all other values + { Zuint16, Cx0400, 0x0000, Z_(Illuminance), 1, 0 }, // Illuminance (in Lux) + { Zuint16, Cx0400, 0x0001, Z_(IlluminanceMinMeasuredValue), 1, 0 }, // + { Zuint16, Cx0400, 0x0002, Z_(IlluminanceMaxMeasuredValue), 1, 0 }, // + { Zuint16, Cx0400, 0x0003, Z_(IlluminanceTolerance), 1, 0 }, // + { Zenum8, Cx0400, 0x0004, Z_(IlluminanceLightSensorType), 1, 0 }, // + { Zunk, Cx0400, 0xFFFF, Z_(), 0, 0 }, // Remove all other values // Illuminance Level Sensing cluster - { Zenum8, Cx0401, 0x0000, Z_(IlluminanceLevelStatus), 1 }, // Illuminance (in Lux) - { Zenum8, Cx0401, 0x0001, Z_(IlluminanceLightSensorType), 1 }, // LightSensorType - { Zuint16, Cx0401, 0x0010, Z_(IlluminanceTargetLevel), 1 }, // - { Zunk, Cx0401, 0xFFFF, Z_(), 0 }, // Remove all other values + { Zenum8, Cx0401, 0x0000, Z_(IlluminanceLevelStatus), 1, 0 }, // Illuminance (in Lux) + { Zenum8, Cx0401, 0x0001, Z_(IlluminanceLightSensorType), 1, 0 }, // LightSensorType + { Zuint16, Cx0401, 0x0010, Z_(IlluminanceTargetLevel), 1, 0 }, // + { Zunk, Cx0401, 0xFFFF, Z_(), 0, 0 }, // Remove all other values // Temperature Measurement cluster - { Zint16, Cx0402, 0x0000, Z_(Temperature), -100 }, // divide by 100 - { Zint16, Cx0402, 0x0001, Z_(TemperatureMinMeasuredValue), -100 }, // - { Zint16, Cx0402, 0x0002, Z_(TemperatureMaxMeasuredValue), -100 }, // - { Zuint16, Cx0402, 0x0003, Z_(TemperatureTolerance), -100 }, // - { Zunk, Cx0402, 0xFFFF, Z_(), 0 }, // Remove all other values + { Zint16, Cx0402, 0x0000, Z_(Temperature), -100, Z_MAPPING(Z_Data_Thermo, temperature) }, + { Zint16, Cx0402, 0x0001, Z_(TemperatureMinMeasuredValue), -100, 0 }, // + { Zint16, Cx0402, 0x0002, Z_(TemperatureMaxMeasuredValue), -100, 0 }, // + { Zuint16, Cx0402, 0x0003, Z_(TemperatureTolerance), -100, 0 }, // + { Zunk, Cx0402, 0xFFFF, Z_(), 0, 0 }, // Remove all other values // Pressure Measurement cluster - { Zint16, Cx0403, 0x0000, Z_(Pressure), 1 }, // Pressure - { Zint16, Cx0403, 0x0001, Z_(PressureMinMeasuredValue), 1 }, // - { Zint16, Cx0403, 0x0002, Z_(PressureMaxMeasuredValue), 1 }, // - { Zuint16, Cx0403, 0x0003, Z_(PressureTolerance), 1 }, // - { Zint16, Cx0403, 0x0010, Z_(PressureScaledValue), 1 }, // - { Zint16, Cx0403, 0x0011, Z_(PressureMinScaledValue), 1 }, // - { Zint16, Cx0403, 0x0012, Z_(PressureMaxScaledValue), 1 }, // - { Zuint16, Cx0403, 0x0013, Z_(PressureScaledTolerance), 1 }, // - { Zint8, Cx0403, 0x0014, Z_(PressureScale), 1 }, // - { Zint16, Cx0403, 0xFF00, Z_(SeaPressure), 1 }, // Pressure at Sea Level, Tasmota specific - { Zunk, Cx0403, 0xFFFF, Z_(), 0 }, // Remove all other Pressure values + { Zint16, Cx0403, 0x0000, Z_(Pressure), 1, Z_MAPPING(Z_Data_Thermo, pressure) }, // Pressure + { Zint16, Cx0403, 0x0001, Z_(PressureMinMeasuredValue), 1, 0 }, // + { Zint16, Cx0403, 0x0002, Z_(PressureMaxMeasuredValue), 1, 0 }, // + { Zuint16, Cx0403, 0x0003, Z_(PressureTolerance), 1, 0 }, // + { Zint16, Cx0403, 0x0010, Z_(PressureScaledValue), 1, 0 }, // + { Zint16, Cx0403, 0x0011, Z_(PressureMinScaledValue), 1, 0 }, // + { Zint16, Cx0403, 0x0012, Z_(PressureMaxScaledValue), 1, 0 }, // + { Zuint16, Cx0403, 0x0013, Z_(PressureScaledTolerance), 1, 0 }, // + { Zint8, Cx0403, 0x0014, Z_(PressureScale), 1, 0 }, // + { Zint16, Cx0403, 0xFF00, Z_(SeaPressure), 1, Z_MAPPING(Z_Data_Thermo, pressure) }, // Pressure at Sea Level, Tasmota specific + { Zunk, Cx0403, 0xFFFF, Z_(), 0, 0 }, // Remove all other Pressure values // Flow Measurement cluster - { Zuint16, Cx0404, 0x0000, Z_(FlowRate), -10 }, // Flow (in m3/h) - { Zuint16, Cx0404, 0x0001, Z_(FlowMinMeasuredValue), 1 }, // - { Zuint16, Cx0404, 0x0002, Z_(FlowMaxMeasuredValue), 1 }, // - { Zuint16, Cx0404, 0x0003, Z_(FlowTolerance), 1 }, // - { Zunk, Cx0404, 0xFFFF, Z_(), 0 }, // Remove all other values + { Zuint16, Cx0404, 0x0000, Z_(FlowRate), -10, 0 }, // Flow (in m3/h) + { Zuint16, Cx0404, 0x0001, Z_(FlowMinMeasuredValue), 1, 0 }, // + { Zuint16, Cx0404, 0x0002, Z_(FlowMaxMeasuredValue), 1, 0 }, // + { Zuint16, Cx0404, 0x0003, Z_(FlowTolerance), 1, 0 }, // + { Zunk, Cx0404, 0xFFFF, Z_(), 0, 0 }, // Remove all other values // Relative Humidity Measurement cluster - { Zuint16, Cx0405, 0x0000, Z_(Humidity), -100 }, // Humidity - { Zuint16, Cx0405, 0x0001, Z_(HumidityMinMeasuredValue), 1 }, // - { Zuint16, Cx0405, 0x0002, Z_(HumidityMaxMeasuredValue), 1 }, // - { Zuint16, Cx0405, 0x0003, Z_(HumidityTolerance), 1 }, // - { Zunk, Cx0405, 0xFFFF, Z_(), 0 }, // Remove all other values + { Zuint16, Cx0405, 0x0000, Z_(Humidity), -100, Z_MAPPING(Z_Data_Thermo, humidity) }, // Humidity + { Zuint16, Cx0405, 0x0001, Z_(HumidityMinMeasuredValue), 1, 0 }, // + { Zuint16, Cx0405, 0x0002, Z_(HumidityMaxMeasuredValue), 1, 0 }, // + { Zuint16, Cx0405, 0x0003, Z_(HumidityTolerance), 1, 0 }, // + { Zunk, Cx0405, 0xFFFF, Z_(), 0, 0 }, // Remove all other values // Occupancy Sensing cluster - { Zmap8, Cx0406, 0x0000, Z_(Occupancy), 1 }, // Occupancy (map8) - { Zenum8, Cx0406, 0x0001, Z_(OccupancySensorType), 1 }, // OccupancySensorType - { Zunk, Cx0406, 0xFFFF, Z_(), 0 }, // Remove all other values + { Zmap8, Cx0406, 0x0000, Z_(Occupancy), 1, 0 }, // Occupancy (map8) + { Zenum8, Cx0406, 0x0001, Z_(OccupancySensorType), 1, 0 }, // OccupancySensorType + { Zunk, Cx0406, 0xFFFF, Z_(), 0, 0 }, // Remove all other values // IAS Cluster (Intruder Alarm System) - { Zenum8, Cx0500, 0x0000, Z_(ZoneState), 1 }, // Occupancy (map8) - { Zenum16, Cx0500, 0x0001, Z_(ZoneType), 1 }, // Occupancy (map8) - { Zmap16, Cx0500, 0x0002, Z_(ZoneStatus), 1 }, // Occupancy (map8) + { Zenum8, Cx0500, 0x0000, Z_(ZoneState), 1, 0 }, // Occupancy (map8) + { Zenum16, Cx0500, 0x0001, Z_(ZoneType), 1, 0 }, // Occupancy (map8) + { Zmap16, Cx0500, 0x0002, Z_(ZoneStatus), 1, Z_MAPPING(Z_Data_Alarm, zone_type) }, // Occupancy (map8) // Metering (Smart Energy) cluster - { Zuint48, Cx0702, 0x0000, Z_(CurrentSummDelivered), 1 }, + { Zuint48, Cx0702, 0x0000, Z_(CurrentSummDelivered), 1, 0 }, // Meter Identification cluster - { Zstring, Cx0B01, 0x0000, Z_(CompanyName), 1 }, - { Zuint16, Cx0B01, 0x0001, Z_(MeterTypeID), 1 }, - { Zuint16, Cx0B01, 0x0004, Z_(DataQualityID), 1 }, - { Zstring, Cx0B01, 0x0005, Z_(CustomerName), 1 }, - { Zoctstr, Cx0B01, 0x0006, Z_(Model), 1 }, - { Zoctstr, Cx0B01, 0x0007, Z_(PartNumber), 1 }, - { Zoctstr, Cx0B01, 0x0008, Z_(ProductRevision), 1 }, - { Zoctstr, Cx0B01, 0x000A, Z_(SoftwareRevision), 1 }, - { Zstring, Cx0B01, 0x000B, Z_(UtilityName), 1 }, - { Zstring, Cx0B01, 0x000C, Z_(POD), 1 }, - { Zint24, Cx0B01, 0x000D, Z_(AvailablePower), 1 }, - { Zint24, Cx0B01, 0x000E, Z_(PowerThreshold), 1 }, + { Zstring, Cx0B01, 0x0000, Z_(CompanyName), 1, 0 }, + { Zuint16, Cx0B01, 0x0001, Z_(MeterTypeID), 1, 0 }, + { Zuint16, Cx0B01, 0x0004, Z_(DataQualityID), 1, 0 }, + { Zstring, Cx0B01, 0x0005, Z_(CustomerName), 1, 0 }, + { Zoctstr, Cx0B01, 0x0006, Z_(Model), 1, 0 }, + { Zoctstr, Cx0B01, 0x0007, Z_(PartNumber), 1, 0 }, + { Zoctstr, Cx0B01, 0x0008, Z_(ProductRevision), 1, 0 }, + { Zoctstr, Cx0B01, 0x000A, Z_(SoftwareRevision), 1, 0 }, + { Zstring, Cx0B01, 0x000B, Z_(UtilityName), 1, 0 }, + { Zstring, Cx0B01, 0x000C, Z_(POD), 1, 0 }, + { Zint24, Cx0B01, 0x000D, Z_(AvailablePower), 1, 0 }, + { Zint24, Cx0B01, 0x000E, Z_(PowerThreshold), 1, 0 }, // Electrical Measurement cluster - { Zuint16, Cx0B04, 0x0505, Z_(RMSVoltage), 1 }, - { Zuint16, Cx0B04, 0x0508, Z_(RMSCurrent), 1 }, - { Zint16, Cx0B04, 0x050B, Z_(ActivePower), 1 }, + { Zuint16, Cx0B04, 0x0505, Z_(RMSVoltage), 1, Z_MAPPING(Z_Data_Plug, mains_voltage) }, + { Zuint16, Cx0B04, 0x0508, Z_(RMSCurrent), 1, 0 }, + { Zint16, Cx0B04, 0x050B, Z_(ActivePower), 1, Z_MAPPING(Z_Data_Plug, mains_power) }, // Diagnostics cluster - { Zuint16, Cx0B05, 0x0000, Z_(NumberOfResets), 1 }, - { Zuint16, Cx0B05, 0x0001, Z_(PersistentMemoryWrites),1 }, - { Zuint8, Cx0B05, 0x011C, Z_(LastMessageLQI), 1 }, - { Zuint8, Cx0B05, 0x011D, Z_(LastMessageRSSI), 1 }, + { Zuint16, Cx0B05, 0x0000, Z_(NumberOfResets), 1, 0 }, + { Zuint16, Cx0B05, 0x0001, Z_(PersistentMemoryWrites),1, 0 }, + { Zuint8, Cx0B05, 0x011C, Z_(LastMessageLQI), 1, 0 }, + { Zuint8, Cx0B05, 0x011D, Z_(LastMessageRSSI), 1, 0 }, }; - +#pragma GCC diagnostic pop typedef union ZCLHeaderFrameControl_t { struct { @@ -1673,8 +1681,10 @@ void ZCLFrame::postProcessAttributes(uint16_t shortaddr, Z_attribute_list& attr_ // Look for an entry in the converter table bool found = false; - int8_t conv_multiplier; const char * conv_name; + Z_Data_Type map_type; + uint8_t map_offset; + uint8_t zigbee_type; for (uint32_t i = 0; i < ARRAY_SIZE(Z_PostProcess); i++) { const Z_AttributeConverter *converter = &Z_PostProcess[i]; uint16_t conv_cluster = CxToCluster(pgm_read_byte(&converter->cluster_short)); @@ -1682,7 +1692,10 @@ void ZCLFrame::postProcessAttributes(uint16_t shortaddr, Z_attribute_list& attr_ if ((conv_cluster == cluster) && ((conv_attribute == attribute) || (conv_attribute == 0xFFFF)) ) { - conv_multiplier = pgm_read_byte(&converter->multiplier); + 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; @@ -1691,42 +1704,39 @@ void ZCLFrame::postProcessAttributes(uint16_t shortaddr, Z_attribute_list& attr_ // apply multiplier if needed float fval = attr.getFloat(); - 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); } - attr.setFloat(fval); + if (found && (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 + + // 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; + uint32_t uval32 = attr.getUInt(); // call converter to uint only once + int32_t ival32 = attr.getInt(); // call converter to int only once + // AddLog_P2(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); + switch (zigbee_type) { + case Zenum8: + case Zuint8: *(uint8_t*)attr_address = uval32; break; + case Zenum16: + case Zuint16: *(uint16_t*)attr_address = uval32; break; + case Zuint32: *(uint32_t*)attr_address = uval32; break; + case Zint8: *(int8_t*)attr_address = ival32; break; + case Zint16: *(int16_t*)attr_address = ival32; break; + case Zint32: *(int32_t*)attr_address = ival32; break; } } uint16_t uval16 = attr.getUInt(); // call converter to uint only once int16_t ival16 = attr.getInt(); // call converter to int only once + Z_Data_Set & data = device.data; // update any internal structure switch (ccccaaaa) { case 0x00000004: zigbee_devices.setManufId(shortaddr, attr.getStr()); break; case 0x00000005: zigbee_devices.setModelId(shortaddr, attr.getStr()); break; case 0x00010021: zigbee_devices.setBatteryPercent(shortaddr, uval16); break; case 0x00060000: - case 0x00068000: device.setPower(attr.getBool()); break; - case 0x00080000: device.dimmer = uval16; break; - case 0x02010000: device.temperature = fval * 10 + 0.5f; break; - case 0x02010008: device.th_setpoint = uval16; break; - case 0x02010012: device.temperature_target = fval * 10 + 0.5f; break; - case 0x02010007: device.th_setpoint = uval16; break; - case 0x02010011: device.temperature_target = fval * 10 + 0.5f; break; - case 0x03000000: device.hue = changeUIntScale(uval16, 0, 254, 0, 360); break; - case 0x03000001: device.sat = uval16; break; - case 0x03000003: device.x = uval16; break; - case 0x03000004: device.y = uval16; break; - case 0x03000007: device.ct = uval16; break; - case 0x03000008: device.colormode = uval16; break; - case 0x04020000: device.temperature = fval * 10 + 0.5f; break; - case 0x0403FF00: device.pressure = fval + 0.5f; break; // give priority to sea level - case 0x04030000: device.pressure = fval + 0.5f; break; - case 0x04050000: device.humidity = fval + 0.5f; break; - case 0x0B040505: device.mains_voltage = uval16; break; - case 0x0B04050B: device.mains_power = ival16; break; + case 0x00068000: device.setPower(attr.getBool(), src_ep); break; } // Replace cluster/attribute with name diff --git a/tasmota/xdrv_23_zigbee_A_impl.ino b/tasmota/xdrv_23_zigbee_A_impl.ino index 3e29c7962..04c7e3c5f 100644 --- a/tasmota/xdrv_23_zigbee_A_impl.ino +++ b/tasmota/xdrv_23_zigbee_A_impl.ino @@ -33,7 +33,7 @@ const char kZbCommands[] PROGMEM = D_PRFX_ZB "|" // prefix D_CMND_ZIGBEE_FORGET "|" D_CMND_ZIGBEE_SAVE "|" D_CMND_ZIGBEE_NAME "|" D_CMND_ZIGBEE_BIND "|" D_CMND_ZIGBEE_UNBIND "|" D_CMND_ZIGBEE_PING "|" D_CMND_ZIGBEE_MODELID "|" D_CMND_ZIGBEE_LIGHT "|" D_CMND_ZIGBEE_RESTORE "|" D_CMND_ZIGBEE_BIND_STATE "|" - D_CMND_ZIGBEE_CONFIG + D_CMND_ZIGBEE_CONFIG "|" D_CMND_ZIGBEE_DATA ; void (* const ZigbeeCommand[])(void) PROGMEM = { @@ -48,7 +48,7 @@ void (* const ZigbeeCommand[])(void) PROGMEM = { &CmndZbForget, &CmndZbSave, &CmndZbName, &CmndZbBind, &CmndZbUnbind, &CmndZbPing, &CmndZbModelId, &CmndZbLight, &CmndZbRestore, &CmndZbBindState, - &CmndZbConfig, + &CmndZbConfig, CmndZbData, }; /********************************************************************************************/ @@ -56,6 +56,10 @@ void (* const ZigbeeCommand[])(void) PROGMEM = { // Initialize internal structures void ZigbeeInit(void) { +// #pragma GCC diagnostic push +// #pragma GCC diagnostic ignored "-Winvalid-offsetof" +// Serial.printf(">>> offset %d %d %d\n", Z_offset(Z_Data_Light, dimmer), Z_offset(Z_Data_Light, x), Z_offset(Z_Data_Thermo, temperature)); +// #pragma GCC diagnostic pop // Check if settings in Flash are set if (PinUsed(GPIO_ZIGBEE_RX) && PinUsed(GPIO_ZIGBEE_TX)) { if (0 == Settings.zb_channel) { @@ -1018,7 +1022,7 @@ void CmndZbLight(void) { int8_t bulbtype = strtol(p, nullptr, 10); if (bulbtype > 5) { bulbtype = 5; } if (bulbtype < -1) { bulbtype = -1; } - zigbee_devices.setHueBulbtype(shortaddr, bulbtype); + zigbee_devices.setLightProfile(shortaddr, bulbtype); } String dump = zigbee_devices.dumpLightState(shortaddr); Response_P(PSTR("{\"" D_PRFX_ZB D_CMND_ZIGBEE_LIGHT "\":%s}"), dump.c_str()); @@ -1203,7 +1207,7 @@ void CmndZbStatus(void) { if (ZigbeeSerial) { if (zigbee.init_phase) { ResponseCmndChar_P(PSTR(D_ZIGBEE_NOT_STARTED)); return; } uint16_t shortaddr = zigbee_devices.parseDeviceParam(XdrvMailbox.data); - if (XdrvMailbox.payload > 0) { + if (XdrvMailbox.data_len > 0) { if (BAD_SHORTADDR == shortaddr) { ResponseCmndChar_P(PSTR("Unknown device")); return; } } @@ -1212,6 +1216,195 @@ void CmndZbStatus(void) { } } +// +// Innder part of ZbData parsing +// +// {"L-02":{"Dimmer":10,"Sat":254}} +bool parseDeviceInnerData(class Z_Device & device, JsonParserObject root) { + for (auto data_elt : root) { + const char * data_type_str = data_elt.getStr(); + Z_Data_Type data_type; + + switch (data_type_str[0]) { + case 'P': data_type = Z_Data_Type::Z_Plug; break; + case 'L': data_type = Z_Data_Type::Z_Light; break; + case 'O': data_type = Z_Data_Type::Z_OnOff; break; + case 'T': data_type = Z_Data_Type::Z_Thermo; break; + case 'A': data_type = Z_Data_Type::Z_Alarm; break; + case '_': data_type = Z_Data_Type::Z_Device; break; + default: data_type = Z_Data_Type::Z_Unknown; break; + } + // The format should be a valid Code Lette followed by '-' + if (data_type == Z_Data_Type::Z_Unknown) { + Response_P(PSTR("{\"%s\":\"%s \"%s\"\"}"), XdrvMailbox.command, PSTR("Invalid Parameters"), data_type_str); + return false; + } + + JsonParserObject data_values = data_elt.getValue().getObject(); + if (!data_values) { return false; } + + // Decode the endpoint number + uint8_t endpoint = strtoul(&data_type_str[1], nullptr, 16); // hex base 16 + JsonParserToken val; + + switch (data_type) { + case Z_Data_Type::Z_Plug: + { + Z_Data_Plug & plug = device.data.get(endpoint); + + if (val = data_values[PSTR("RMSVoltage")]) { plug.setMainsVoltage(val.getUInt()); } + if (val = data_values[PSTR("ActivePower")]) { plug.setMainsPower(val.getInt()); } + } + break; + case Z_Data_Type::Z_Light: + { + Z_Data_Light & light = device.data.get(endpoint); + + if (val = data_values[PSTR("Light")]) { light.setConfig(val.getUInt()); } + if (val = data_values[PSTR("Dimmer")]) { light.setDimmer(val.getUInt()); } + if (val = data_values[PSTR("Colormode")]) { light.setColorMode(val.getUInt()); } + if (val = data_values[PSTR("CT")]) { light.setCT(val.getUInt()); } + if (val = data_values[PSTR("Sat")]) { light.setSat(val.getUInt()); } + if (val = data_values[PSTR("Hue")]) { light.setHue(val.getUInt()); } + if (val = data_values[PSTR("X")]) { light.setX(val.getUInt()); } + if (val = data_values[PSTR("Y")]) { light.setY(val.getUInt()); } + } + break; + case Z_Data_Type::Z_OnOff: + { + Z_Data_OnOff & onoff = device.data.get(endpoint); + + if (val = data_values[PSTR("Power")]) { onoff.setPower(val.getUInt() ? true : false); } + } + break; + case Z_Data_Type::Z_Thermo: + { + Z_Data_Thermo & thermo = device.data.get(endpoint); + + if (val = data_values[PSTR("Temperature")]) { thermo.setTemperature(val.getInt()); } + if (val = data_values[PSTR("Pressure")]) { thermo.setPressure(val.getUInt()); } + if (val = data_values[PSTR("Humidity")]) { thermo.setHumidity(val.getUInt()); } + if (val = data_values[PSTR("ThSetpoint")]) { thermo.setThSetpoint(val.getUInt()); } + if (val = data_values[PSTR("TempTarget")]) { thermo.setTempTarget(val.getInt()); } + } + break; + case Z_Data_Type::Z_Alarm: + { + Z_Data_Alarm & alarm = device.data.get(endpoint); + + if (val = data_values[PSTR("ZoneType")]) { alarm.setZoneType(val.getUInt()); } + } + break; + case Z_Data_Type::Z_Device: + { + if (val = data_values[PSTR(D_CMND_ZIGBEE_LINKQUALITY)]) { device.lqi = val.getUInt(); } + if (val = data_values[PSTR("BatteryPercentage")]) { device.batterypercent = val.getUInt(); } + if (val = data_values[PSTR("LastSeen")]) { device.last_seen = val.getUInt(); } + } + break; + } + } + return true; +} + +// +// Command `ZbData` +// +void CmndZbData(void) { + if (zigbee.init_phase) { ResponseCmndChar_P(PSTR(D_ZIGBEE_NOT_STARTED)); return; } + RemoveAllSpaces(XdrvMailbox.data); + if (XdrvMailbox.data[0] == '{') { + // JSON input, enter saved data into memory -- essentially for debugging + JsonParser parser(XdrvMailbox.data); + JsonParserObject root = parser.getRootObject(); + if (!root) { ResponseCmndChar_P(PSTR(D_JSON_INVALID_JSON)); return; } + + // Skip `ZbData` if present + JsonParserToken zbdata = root.getObject().findStartsWith(PSTR("ZbData")); + if (zbdata) { + root = zbdata; + } + + for (auto device_name : root) { + uint16_t shortaddr = zigbee_devices.parseDeviceParam(device_name.getStr()); + if (BAD_SHORTADDR == shortaddr) { ResponseCmndChar_P(PSTR("Unknown device")); return; } + Z_Device & device = zigbee_devices.getShortAddr(shortaddr); + JsonParserObject inner_data = device_name.getValue().getObject(); + if (inner_data) { + if (!parseDeviceInnerData(device, inner_data)) { + return; + } + } + } + ResponseCmndDone(); + } else { + // non-JSON, export current data + // ZbData 0x1234 + // ZbData Device_Name + uint16_t shortaddr = zigbee_devices.parseDeviceParam(XdrvMailbox.data); + if (BAD_SHORTADDR == shortaddr) { ResponseCmndChar_P(PSTR("Unknown device")); return; } + const Z_Device & device = zigbee_devices.findShortAddr(shortaddr); + + Z_attribute_list attr_data; + + { // scope to force object deallocation + Z_attribute_list device_attr; + device.toAttributes(device_attr); + attr_data.addAttribute(F("_00")).setStrRaw(device_attr.toString(true).c_str()); + } + + // Iterate on data objects + for (auto & data_elt : device.data) { + Z_attribute_list inner_attr; + char key[4]; + snprintf_P(key, sizeof(key), "?%02X", data_elt.getEndpoint()); + // The key is in the form "L-01", where 'L' is the type and '01' the endpoint in hex format + // 'L' = Light + // 'P' = Power + // + switch (data_elt.getType()) { + case Z_Data_Type::Z_Plug: + { + key[0] = 'P'; + Z_Data_Plug::toAttributes(inner_attr, (Z_Data_Plug&) data_elt); + } + break; + case Z_Data_Type::Z_Light: + { + key[0] = 'L'; + Z_Data_Light::toAttributes(inner_attr, (Z_Data_Light&) data_elt); + } + break; + case Z_Data_Type::Z_OnOff: + { + key[0] = 'O'; + Z_Data_OnOff::toAttributes(inner_attr, (Z_Data_OnOff&) data_elt); + } + break; + case Z_Data_Type::Z_Thermo: + { + key[0] = 'T'; + Z_Data_Thermo::toAttributes(inner_attr, (Z_Data_Thermo&) data_elt); + } + break; + case Z_Data_Type::Z_Alarm: + { + key[0] = 'A'; + Z_Data_Alarm::toAttributes(inner_attr, (Z_Data_Alarm&) data_elt); + } + break; + } + if (key[0] != '?') { + attr_data.addAttribute(key).setStrRaw(inner_attr.toString(true).c_str()); + } + } + + char hex[8]; + snprintf_P(hex, sizeof(hex), PSTR("0x%04X"), shortaddr); + Response_P(PSTR("{\"%s\":{\"%s\":%s}}"), XdrvMailbox.command, hex, attr_data.toString(true).c_str()); + } +} + // // Command `ZbConfig` // @@ -1431,67 +1624,71 @@ void ZigbeeShow(bool json) ), dhm ); // Sensors - bool temperature_ok = device.validTemperature(); - bool tempareture_target_ok = device.validTemperatureTarget(); - bool th_setpoint_ok = device.validThSetpoint(); - bool humidity_ok = device.validHumidity(); - bool pressure_ok = device.validPressure(); + const Z_Data_Thermo & thermo = device.data.find(); - if (temperature_ok || tempareture_target_ok || th_setpoint_ok || humidity_ok || pressure_ok) { + if (&thermo != nullptr) { WSContentSend_P(PSTR("┆")); - if (temperature_ok) { + if (thermo.validTemperature()) { char buf[12]; - dtostrf(device.temperature / 10.0f, 3, 1, buf); + dtostrf(thermo.getTemperature() / 100.0f, 3, 1, buf); WSContentSend_PD(PSTR(" ☀️ %s°C"), buf); } - if (tempareture_target_ok) { + if (thermo.validTempTarget()) { char buf[12]; - dtostrf(device.temperature_target / 10.0f, 3, 1, buf); + dtostrf(thermo.getTempTarget() / 100.0f, 3, 1, buf); WSContentSend_PD(PSTR(" 🎯 %s°C"), buf); } - if (th_setpoint_ok) { - WSContentSend_PD(PSTR(" ⚙️ %d%%"), device.th_setpoint); + if (thermo.validThSetpoint()) { + WSContentSend_PD(PSTR(" ⚙️ %d%%"), thermo.getThSetpoint()); } - if (humidity_ok) { - WSContentSend_P(PSTR(" 💧 %d%%"), device.humidity); + if (thermo.validHumidity()) { + WSContentSend_P(PSTR(" 💧 %d%%"), (uint16_t)(thermo.getHumidity() / 100.0f + 0.5f)); } - if (pressure_ok) { - WSContentSend_P(PSTR(" ⛅ %d hPa"), device.pressure); + if (thermo.validPressure()) { + WSContentSend_P(PSTR(" ⛅ %d hPa"), thermo.getPressure()); } WSContentSend_P(PSTR("{e}")); } // Light, switches and plugs - bool power_ok = device.validPower(); - if (power_ok) { - uint8_t channels = device.getLightChannels(); - if (0xFF == channels) { channels = 5; } // if number of channel is unknown, display all known attributes - WSContentSend_P(PSTR("┆ %s"), device.getPower() ? PSTR(D_ON) : PSTR(D_OFF)); - if (device.validDimmer() && (channels >= 1)) { - WSContentSend_P(PSTR(" 🔅 %d%%"), changeUIntScale(device.dimmer,0,254,0,100)); + const Z_Data_OnOff & onoff = device.data.find(); + const Z_Data_Light & light = device.data.find(); + bool light_display = (&light != nullptr) ? light.validDimmer() : false; + const Z_Data_Plug & plug = device.data.find(); + if ((&onoff != nullptr) || light_display || (&plug != nullptr)) { + int8_t channels = device.getLightChannels(); + if (channels < 0) { channels = 5; } // if number of channel is unknown, display all known attributes + WSContentSend_P(PSTR("┆")); + if (&onoff != nullptr) { + WSContentSend_P(PSTR(" %s"), device.getPower() ? PSTR(D_ON) : PSTR(D_OFF)); } - if (device.validCT() && ((channels == 2) || (channels == 5))) { - uint32_t ct_k = (((1000000 / device.ct) + 25) / 50) * 50; - WSContentSend_P(PSTR(" %dK"), device.ct, ct_k); - } - if (device.validHue() && device.validSat() && (channels >= 3)) { - uint8_t r,g,b; - uint8_t sat = changeUIntScale(device.sat, 0, 254, 0, 255); // scale to 0..255 - LightStateClass::HsToRgb(device.hue, sat, &r, &g, &b); - WSContentSend_P(PSTR(" #%02X%02X%02X"), r,g,b,r,g,b); - } else if (device.validX() && device.validY() && (channels >= 3)) { - uint8_t r,g,b; - LightStateClass::XyToRgb(device.x / 65535.0f, device.y / 65535.0f, &r, &g, &b); - WSContentSend_P(PSTR(" #%02X%02X%02X"), r,g,b,r,g,b); - } - if (device.validMainsPower() || device.validMainsVoltage()) { - WSContentSend_P(PSTR(" ⚡ ")); - if (device.validMainsVoltage()) { - WSContentSend_P(PSTR(" %dV"), device.mains_voltage); + if (&light != nullptr) { + if (light.validDimmer() && (channels >= 1)) { + WSContentSend_P(PSTR(" 🔅 %d%%"), changeUIntScale(light.getDimmer(),0,254,0,100)); } - if (device.validMainsPower()) { - WSContentSend_P(PSTR(" %dW"), device.mains_power); + if (light.validCT() && ((channels == 2) || (channels == 5))) { + uint32_t ct_k = (((1000000 / light.getCT()) + 25) / 50) * 50; + WSContentSend_P(PSTR(" %dK"), light.getCT(), ct_k); + } + if (light.validHue() && light.validSat() && (channels >= 3)) { + uint8_t r,g,b; + uint8_t sat = changeUIntScale(light.getSat(), 0, 254, 0, 255); // scale to 0..255 + LightStateClass::HsToRgb(light.getHue(), sat, &r, &g, &b); + WSContentSend_P(PSTR(" #%02X%02X%02X"), r,g,b,r,g,b); + } else if (light.validX() && light.validY() && (channels >= 3)) { + uint8_t r,g,b; + LightStateClass::XyToRgb(light.getX() / 65535.0f, light.getY() / 65535.0f, &r, &g, &b); + WSContentSend_P(PSTR(" #%02X%02X%02X"), r,g,b,r,g,b); + } + } + if (&plug != nullptr) { + WSContentSend_P(PSTR(" ⚡ ")); + if (plug.validMainsVoltage()) { + WSContentSend_P(PSTR(" %dV"), plug.getMainsVoltage()); + } + if (plug.validMainsPower()) { + WSContentSend_P(PSTR(" %dW"), plug.getMainsPower()); } } WSContentSend_P(PSTR("{e}"));