/* xdrv_23_zigbee_converters.ino - zigbee support for Sonoff-Tasmota Copyright (C) 2019 Theo Arends and Stephan Hadinger This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifdef USE_ZIGBEE /*********************************************************************************************\ * ZCL \*********************************************************************************************/ typedef union ZCLHeaderFrameControl_t { struct { uint8_t frame_type : 2; // 00 = across entire profile, 01 = cluster specific uint8_t manuf_specific : 1; // Manufacturer Specific Sub-field uint8_t direction : 1; // 0 = tasmota to zigbee, 1 = zigbee to tasmota uint8_t disable_def_resp : 1; // don't send back default response uint8_t reserved : 3; } b; uint32_t d8; // raw 8 bits field } ZCLHeaderFrameControl_t; class ZCLFrame { public: ZCLFrame(uint8_t frame_control, uint16_t manuf_code, uint8_t transact_seq, uint8_t cmd_id, const char *buf, size_t buf_len, uint16_t clusterid = 0, uint16_t groupid = 0): _cmd_id(cmd_id), _manuf_code(manuf_code), _transact_seq(transact_seq), _payload(buf_len ? buf_len : 250), // allocate the data frame from source or preallocate big enough _cluster_id(clusterid), _group_id(groupid) { _frame_control.d8 = frame_control; _payload.addBuffer(buf, buf_len); }; void publishMQTTReceived(uint16_t groupid, uint16_t clusterid, Z_ShortAddress srcaddr, uint8_t srcendpoint, uint8_t dstendpoint, uint8_t wasbroadcast, uint8_t linkquality, uint8_t securityuse, uint8_t seqnumber, uint32_t timestamp) { char hex_char[_payload.len()*2+2]; ToHex_P((unsigned char*)_payload.getBuffer(), _payload.len(), hex_char, sizeof(hex_char)); Response_P(PSTR("{\"" D_JSON_ZIGBEEZCLRECEIVED "\":{" "\"groupid\":%d," "\"clusterid\":%d," "\"srcaddr\":\"0x%04X\"," "\"srcendpoint\":%d," "\"dstendpoint\":%d," "\"wasbroadcast\":%d," "\"linkquality\":%d," "\"securityuse\":%d," "\"seqnumber\":%d," "\"timestamp\":%d," "\"fc\":\"0x%02X\",\"manuf\":\"0x%04X\",\"transact\":%d," "\"cmdid\":\"0x%02X\",\"payload\":\"%s\""), groupid, clusterid, srcaddr, srcendpoint, dstendpoint, wasbroadcast, linkquality, securityuse, seqnumber, timestamp, _frame_control, _manuf_code, _transact_seq, _cmd_id, hex_char); ResponseJsonEnd(); // append '}' ResponseJsonEnd(); // append '}' MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_ZIGBEEZCLSENT)); XdrvRulesProcess(); } static ZCLFrame parseRawFrame(const SBuffer &buf, uint8_t offset, uint8_t len, uint16_t clusterid, uint16_t groupid) { // parse a raw frame and build the ZCL frame object uint32_t i = offset; ZCLHeaderFrameControl_t frame_control; uint16_t manuf_code = 0; uint8_t transact_seq; uint8_t cmd_id; frame_control.d8 = buf.get8(i++); if (frame_control.b.manuf_specific) { manuf_code = buf.get16(i); i += 2; } transact_seq = buf.get8(i++); cmd_id = buf.get8(i++); ZCLFrame zcl_frame(frame_control.d8, manuf_code, transact_seq, cmd_id, (const char *)(buf.buf() + i), len + offset - i, clusterid, groupid); return zcl_frame; } bool isClusterSpecificCommand(void) { return _frame_control.b.frame_type & 1; } void parseRawAttributes(JsonObject& json, uint8_t offset = 0); void parseClusterSpecificCommand(JsonObject& json, uint8_t offset = 0); void postProcessAttributes(JsonObject& json); inline void setGroupId(uint16_t groupid) { _group_id = groupid; } inline void setClusterId(uint16_t clusterid) { _cluster_id = clusterid; } inline uint8_t getCmdId(void) const { return _cmd_id; } inline uint16_t getClusterId(void) const { return _cluster_id; } const SBuffer &getPayload(void) const { return _payload; } private: ZCLHeaderFrameControl_t _frame_control = { .d8 = 0 }; uint16_t _manuf_code = 0; // optional uint8_t _transact_seq = 0; // transaction sequence number uint8_t _cmd_id = 0; uint16_t _cluster_id = 0; uint16_t _group_id = 0; SBuffer _payload; }; char Hex36Char(uint8_t value) { // convert an integer from 0 to 46, to a single digit 0-9A-Z if (value < 10) { return '0' + value; } else if (value < 46) { return 'A' + value - 10; } else { return '?'; // out of range } } // Zigbee ZCL converters // from https://github.com/Koenkk/zigbee-shepherd-converters/blob/638d29f0cace6343052b9a4e7fd60980fa785479/converters/fromZigbee.js#L55 // Input voltage in mV, i.e. 3000 = 3.000V // Output percentage from 0 to 100 as int uint8_t toPercentageCR2032(uint32_t voltage) { uint32_t percentage; if (voltage < 2100) { percentage = 0; } else if (voltage < 2440) { percentage = 6 - ((2440 - voltage) * 6) / 340; } else if (voltage < 2740) { percentage = 18 - ((2740 - voltage) * 12) / 300; } else if (voltage < 2900) { percentage = 42 - ((2900 - voltage) * 24) / 160; } else if (voltage < 3000) { percentage = 100 - ((3000 - voltage) * 58) / 100; } else if (voltage >= 3000) { percentage = 100; } return percentage; } uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer &buf, uint32_t offset, uint32_t len) { uint32_t i = offset; uint32_t attrtype = buf.get8(i++); // fallback - enter a null value json[attrid_str] = (char*) nullptr; // now parse accordingly to attr type switch (attrtype) { case 0x00: // nodata case 0xFF: // unk break; case 0x10: // bool { uint8_t val_bool = buf.get8(i++); if (0xFF != val_bool) { json[attrid_str] = (bool) (val_bool ? true : false); } } break; case 0x20: // uint8 { uint8_t uint8_val = buf.get8(i); i += 1; if (0xFF != uint8_val) { json[attrid_str] = uint8_val; } } break; case 0x21: // uint16 { uint16_t uint16_val = buf.get16(i); i += 2; if (0xFFFF != uint16_val) { json[attrid_str] = uint16_val; } } break; case 0x23: // uint16 { uint32_t uint32_val = buf.get32(i); i += 4; if (0xFFFFFFFF != uint32_val) { json[attrid_str] = uint32_val; } } break; // Note: uint40, uint48, uint56, uint64 are not used in ZCL, so they are not implemented (yet) case 0x24: // int40 case 0x25: // int48 case 0x26: // int56 case 0x27: // int64 i += attrtype - 0x1F; // 5 - 8; break; case 0x28: // uint8 { int8_t int8_val = buf.get8(i); i += 1; if (0x80 != int8_val) { json[attrid_str] = int8_val; } } break; case 0x29: // uint16 { int16_t int16_val = buf.get16(i); i += 2; if (0x8000 != int16_val) { json[attrid_str] = int16_val; } } break; case 0x2B: // uint16 { int32_t int32_val = buf.get32(i); i += 4; if (0x80000000 != int32_val) { json[attrid_str] = int32_val; } } break; // Note: int40, int48, int56, int64 are not used in ZCL, so they are not implemented (yet) case 0x2C: // int40 case 0x2D: // int48 case 0x2E: // int56 case 0x2F: // int64 i += attrtype - 0x27; // 5 - 8; break; case 0x41: // octet string, 1 byte len case 0x42: // char string, 1 byte len case 0x43: // octet string, 2 bytes len case 0x44: // char string, 2 bytes len // For strings, default is to try to do a real string, but reverts to octet stream if null char is present or on some exceptions { bool parse_as_string = true; uint32_t len = (attrtype <= 0x42) ? buf.get8(i) : buf.get16(i); // len is 8 or 16 bits i += (attrtype <= 0x42) ? 1 : 2; // increment pointer // check if we can safely use a string if ((0x41 == attrtype) || (0x43 == attrtype)) { parse_as_string = false; } else { for (uint32_t j = 0; j < len; j++) { if (0x00 == buf.get8(i+j)) { parse_as_string = false; break; } } } if (parse_as_string) { char str[len+1]; strncpy(str, buf.charptr(i), len); str[len] = 0x00; json[attrid_str] = str; } else { // print as HEX char hex[2*len+1]; ToHex_P(buf.buf(i), len, hex, sizeof(hex)); json[attrid_str] = hex; } i += len; break; } i += buf.get8(i) + 1; break; // TODO case 0x08: // data8 i++; break; case 0x18: // map8 i++; break; case 0x19: // map16 i += 2; break; case 0x1B: // map32 i += 4; break; // enum case 0x30: // enum8 case 0x31: // enum16 i += attrtype - 0x2F; break; case 0x39: // float i += 4; break; case 0xE0: // ToD case 0xE1: // date case 0xE2: // UTC i += 4; break; case 0xE8: // clusterId case 0xE9: // attribId i += 2; break; case 0xEA: // bacOID i += 4; break; case 0xF0: // EUI64 i += 8; break; case 0xF1: // key128 i += 16; break; // Other un-implemented data types case 0x09: // data16 case 0x0A: // data24 case 0x0B: // data32 case 0x0C: // data40 case 0x0D: // data48 case 0x0E: // data56 case 0x0F: // data64 i += attrtype - 0x07; // 2-8 break; // map case 0x1A: // map24 case 0x1C: // map40 case 0x1D: // map48 case 0x1E: // map56 case 0x1F: // map64 i += attrtype - 0x17; break; // semi case 0x38: // semi (float on 2 bytes) i += 2; break; case 0x3A: // double precision i += 8; break; } // String pp; // pretty print // json[attrid_str].prettyPrintTo(pp); // // now store the attribute // AddLog_P2(LOG_LEVEL_INFO, PSTR("ZIG: ZCL attribute decoded, id %s, type 0x%02X, val=%s"), // attrid_str, attrtype, pp.c_str()); return i - offset; // how much have we increased the index } // First pass, parse all attributes in their native format // The key is 32 bits, high 16 bits is cluserid, low 16 bits is attribute id void ZCLFrame::parseRawAttributes(JsonObject& json, uint8_t offset) { uint32_t i = offset; uint32_t len = _payload.len(); while (len - i >= 3) { uint16_t attrid = _payload.get16(i); i += 2; char shortaddr[16]; snprintf_P(shortaddr, sizeof(shortaddr), PSTR("%c_%04X_%04X"), Hex36Char(_cmd_id), _cluster_id, attrid); // exception for Xiaomi lumi.weather - specific field to be treated as octet and not char if ((0x0000 == _cluster_id) && (0xFF01 == attrid)) { if (0x42 == _payload.get8(i)) { _payload.set8(i, 0x41); // change type from 0x42 to 0x41 } } i += parseSingleAttribute(json, shortaddr, _payload, i, len); } } // Parse non-normalized attributes // The key is "s_" followed by 16 bits clusterId, "_" followed by 8 bits command id void ZCLFrame::parseClusterSpecificCommand(JsonObject& json, uint8_t offset) { uint32_t i = offset; uint32_t len = _payload.len(); char attrid_str[12]; snprintf_P(attrid_str, sizeof(attrid_str), PSTR("s_%04X_%02X"), _cluster_id, _cmd_id); char hex_char[_payload.len()*2+2]; ToHex_P((unsigned char*)_payload.getBuffer(), _payload.len(), hex_char, sizeof(hex_char)); json[attrid_str] = hex_char; } // return value: // 0 = keep initial value // 1 = remove initial value typedef int32_t (*Z_AttrConverter)(JsonObject& json, const char *name, JsonVariant& value, const char *new_name, void * param); typedef struct Z_AttributeConverter { const char * filter; const char * name; Z_AttrConverter func; void * param; } Z_AttributeConverter; const float Z_100 PROGMEM = 100.0f; const float Z_10 PROGMEM = 10.0f; // list of post-processing directives const Z_AttributeConverter Z_PostProcess[] = { { "A_0000_0005", D_JSON_MODEL D_JSON_ID, &Z_Copy, nullptr }, // ModelID { "A_0400_0000", D_JSON_ILLUMINANCE, &Z_Copy, nullptr }, // Illuminance (in Lux) { "A_0400_0004", "LightSensorType", &Z_Copy, nullptr }, // LightSensorType { "A_0400_????", nullptr, &Z_Remove, nullptr }, // Remove all other values { "A_0401_0000", "LevelStatus", &Z_Copy, nullptr }, // Illuminance (in Lux) { "A_0401_0001", "LightSensorType", &Z_Copy, nullptr }, // LightSensorType { "A_0401_????", nullptr, &Z_Remove, nullptr }, // Remove all other values { "A_0402_0000", D_JSON_TEMPERATURE, &Z_ConvFloatDivider, (void*) &Z_100 }, // Temperature { "A_0402_????", nullptr, &Z_Remove, nullptr }, // Remove all other values { "A_0403_0000", D_JSON_PRESSURE_UNIT, &Z_Const_Keep, (void*) D_UNIT_PRESSURE}, // Pressure Unit { "A_0403_0000", D_JSON_PRESSURE, &Z_Copy, nullptr }, // Pressure { "A_0403_????", nullptr, &Z_Remove, nullptr }, // Remove all other Pressure values { "A_0404_0000", D_JSON_FLOWRATE, &Z_ConvFloatDivider, (void*) &Z_10 }, // Flow (in m3/h) { "A_0404_????", nullptr, &Z_Remove, nullptr }, // Remove all other values { "A_0405_0000", D_JSON_HUMIDITY, &Z_ConvFloatDivider, (void*) &Z_100 }, // Humidity { "A_0405_????", nullptr, &Z_Remove, nullptr }, // Remove all other values { "A_0406_0000", "Occupancy", &Z_Copy, nullptr }, // Occupancy (map8) { "A_0406_0001", "OccupancySensorType", &Z_Copy, nullptr }, // OccupancySensorType { "A_0406_????", nullptr, &Z_Remove, nullptr }, // Remove all other values // Cmd 0x0A - Cluster 0x0000, attribute 0xFF01 - proprietary { "A_0000_FF01", nullptr, &Z_AqaraSensor, nullptr }, // Occupancy (map8) // // 0x0b04 Electrical Measurement // { "A_0B04_0100", "DCVoltage", &Z_Copy, nullptr }, // Occupancy (map8) // { "A_0B04_0001", "OccupancySensorType", &Z_Copy, nullptr }, // OccupancySensorType // { "A_0B04_????", "", &Z_Remove, nullptr }, // Remove all other values }; // ====================================================================== // Remove attribute int32_t Z_Remove(JsonObject& json, const char *name, JsonVariant& value, const char *new_name, void * param) { return 1; // remove original key } // Copy value as-is int32_t Z_Copy(JsonObject& json, const char *name, JsonVariant& value, const char *new_name, void * param) { json[new_name] = value; return 1; // remove original key } // Copy value as-is int32_t Z_Const_Keep(JsonObject& json, const char *name, JsonVariant& value, const char *new_name, void * param) { json[new_name] = (char*)param; return 0; // keep original key } // Convert int to float with divider int32_t Z_ConvFloatDivider(JsonObject& json, const char *name, JsonVariant& value, const char *new_name, void * param) { float f_value = value; float *divider = (float*) param; json[new_name] = f_value / *divider; return 1; // remove original key } int32_t Z_AqaraSensor(JsonObject& json, const char *name, JsonVariant& value, const char *new_name, void * param) { String hex = value; SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); uint32_t i = 0; uint32_t len = buf2.len(); char shortaddr[8]; char tmp[] = "tmp"; // for obscure reasons, it must be converted from const char* to char*, otherwise ArduinoJson gets confused JsonVariant sub_value; while (len - i >= 2) { uint8_t attrid = buf2.get8(i++); i += parseSingleAttribute(json, tmp, buf2, i, len); float val = json[tmp]; json.remove(tmp); if (0x64 == attrid) { json[F(D_JSON_TEMPERATURE)] = val / 100.0f; } else if (0x65 == attrid) { json[F(D_JSON_HUMIDITY)] = val / 100.0f; } else if (0x66 == attrid) { json[F(D_JSON_PRESSURE)] = val / 100.0f; json[F(D_JSON_PRESSURE_UNIT)] = F(D_UNIT_PRESSURE); // hPa } else if (0x01 == attrid) { json[F(D_JSON_VOLTAGE)] = val / 1000.0f; json[F("Battery")] = toPercentageCR2032(val); } } return 1; // remove original key } // ====================================================================== #define ZCL_MODELID "A_0000_0005" // Cmd 0x0A - Cluster 0x0000, attribute 0x05 #define ZCL_TEMPERATURE "A_0402_0000" // Cmd 0x0A - Cluster 0x0402, attribute 0x00 #define ZCL_PRESSURE "A_0403_0000" // Cmd 0x0A - Cluster 0x0403, attribute 0x00 #define ZCL_PRESSURE_SCALED "A_0403_0010" // Cmd 0x0A - Cluster 0x0403, attribute 0x10 #define ZCL_PRESSURE_SCALE "A_0403_0014" // Cmd 0x0A - Cluster 0x0403, attribute 0x14 #define ZCL_HUMIDITY "A_0405_0000" // Cmd 0x0A - Cluster 0x0403, attribute 0x00 #define ZCL_LUMI_WEATHER "A_0000_FF01" // Cmd 0x0A - Cluster 0x0000, attribute 0xFF01 - proprietary // Cluster Specific commands #define ZCL_OO_OFF "s_0006_00" // Cluster 0x0006, cmd 0x00 - On/Off - Off #define ZCL_OO_ON "s_0006_01" // Cluster 0x0006, cmd 0x01 - On/Off - On #define ZCL_COLORTEMP_MOVE "s_0300_0A" // Cluster 0x0300, cmd 0x0A, Move to Color Temp #define ZCL_LC_MOVE "s_0008_00" // Cluster 0x0008, cmd 0x00, Level Control Move to Level #define ZCL_LC_MOVE_1 "s_0008_01" // Cluster 0x0008, cmd 0x01, Level Control Move #define ZCL_LC_STEP "s_0008_02" // Cluster 0x0008, cmd 0x02, Level Control Step #define ZCL_LC_STOP "s_0008_03" // Cluster 0x0008, cmd 0x03, Level Control Stop #define ZCL_LC_MOVE_WOO "s_0008_04" // Cluster 0x0008, cmd 0x04, Level Control Move to Level, with On/Off #define ZCL_LC_MOVE_1_WOO "s_0008_05" // Cluster 0x0008, cmd 0x05, Level Control Move, with On/Off #define ZCL_LC_STEP_WOO "s_0008_06" // Cluster 0x0008, cmd 0x05, Level Control Step, with On/Off #define ZCL_LC_STOP_WOO "s_0008_07" // Cluster 0x0008, cmd 0x07, Level Control Stop // inspired from https://github.com/torvalds/linux/blob/master/lib/glob.c bool mini_glob_match(char const *pat, char const *str) { for (;;) { unsigned char c = *str++; unsigned char d = *pat++; switch (d) { case '?': /* Wildcard: anything but nul */ if (c == '\0') return false; break; case '\\': d = *pat++; /*FALLTHROUGH*/ default: /* Literal character */ if (c == d) { if (d == '\0') return true; break; } return false; /* No point continuing */ } } } void ZCLFrame::postProcessAttributes(JsonObject& json) { // iterate on json elements for (auto kv : json) { String key = kv.key; JsonVariant& value = kv.value; // Iterate on filter for (uint32_t i = 0; i < sizeof(Z_PostProcess) / sizeof(Z_PostProcess[0]); i++) { const Z_AttributeConverter *converter = &Z_PostProcess[i]; if (mini_glob_match(converter->filter, key.c_str())) { int32_t drop = (*converter->func)(json, key.c_str(), value, converter->name, converter->param); if (drop) { json.remove(key); } } } } } //void ZCLFrame::postProcessAttributes2(JsonObject& json) { // void postProcessAttributes2(JsonObject& json) { // const __FlashStringHelper *key; // // // Osram Mini Switch // key = F(ZCL_OO_OFF); // if (json.containsKey(key)) { // json.remove(key); // json[F(D_CMND_POWER)] = F("Off"); // } // key = F(ZCL_OO_ON); // if (json.containsKey(key)) { // json.remove(key); // json[F(D_CMND_POWER)] = F("On"); // } // key = F(ZCL_COLORTEMP_MOVE); // if (json.containsKey(key)) { // String hex = json[key]; // SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); // uint16_t color_temp = buf2.get16(0); // uint16_t transition_time = buf2.get16(2); // json.remove(key); // json[F("ColorTemp")] = color_temp; // json[F("TransitionTime")] = transition_time / 10.0f; // } // key = F(ZCL_LC_MOVE_WOO); // if (json.containsKey(key)) { // String hex = json[key]; // SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); // uint8_t level = buf2.get8(0); // uint16_t transition_time = buf2.get16(1); // json.remove(key); // json[F("Dimmer")] = changeUIntScale(level, 0, 255, 0, 100); // change to percentage // json[F("TransitionTime")] = transition_time / 10.0f; // if (0 == level) { // json[F(D_CMND_POWER)] = F("Off"); // } else { // json[F(D_CMND_POWER)] = F("On"); // } // } // key = F(ZCL_LC_MOVE); // if (json.containsKey(key)) { // String hex = json[key]; // SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); // uint8_t level = buf2.get8(0); // uint16_t transition_time = buf2.get16(1); // json.remove(key); // json[F("Dimmer")] = changeUIntScale(level, 0, 255, 0, 100); // change to percentage // json[F("TransitionTime")] = transition_time / 10.0f; // } // key = F(ZCL_LC_MOVE_1); // if (json.containsKey(key)) { // String hex = json[key]; // SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); // uint8_t move_mode = buf2.get8(0); // uint8_t move_rate = buf2.get8(1); // json.remove(key); // json[F("Move")] = move_mode ? F("Down") : F("Up"); // json[F("Rate")] = move_rate; // } // key = F(ZCL_LC_MOVE_1_WOO); // if (json.containsKey(key)) { // String hex = json[key]; // SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); // uint8_t move_mode = buf2.get8(0); // uint8_t move_rate = buf2.get8(1); // json.remove(key); // json[F("Move")] = move_mode ? F("Down") : F("Up"); // json[F("Rate")] = move_rate; // if (0 == move_mode) { // json[F(D_CMND_POWER)] = F("On"); // } // } // key = F(ZCL_LC_STEP); // if (json.containsKey(key)) { // String hex = json[key]; // SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); // uint8_t step_mode = buf2.get8(0); // uint8_t step_size = buf2.get8(1); // uint16_t transition_time = buf2.get16(2); // json.remove(key); // json[F("Step")] = step_mode ? F("Down") : F("Up"); // json[F("StepSize")] = step_size; // json[F("TransitionTime")] = transition_time / 10.0f; // } // key = F(ZCL_LC_STEP_WOO); // if (json.containsKey(key)) { // String hex = json[key]; // SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); // uint8_t step_mode = buf2.get8(0); // uint8_t step_size = buf2.get8(1); // uint16_t transition_time = buf2.get16(2); // json.remove(key); // json[F("Step")] = step_mode ? F("Down") : F("Up"); // json[F("StepSize")] = step_size; // json[F("TransitionTime")] = transition_time / 10.0f; // if (0 == step_mode) { // json[F(D_CMND_POWER)] = F("On"); // } // } // key = F(ZCL_LC_STOP); // if (json.containsKey(key)) { // json.remove(key); // json[F("Stop")] = 1; // } // key = F(ZCL_LC_STOP_WOO); // if (json.containsKey(key)) { // json.remove(key); // json[F("Stop")] = 1; // } // // // Lumi.weather proprietary field // key = F(ZCL_LUMI_WEATHER); // if (json.containsKey(key)) { // String hex = json[key]; // SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); // DynamicJsonBuffer jsonBuffer; // JsonObject& json_lumi = jsonBuffer.createObject(); // uint32_t i = 0; // uint32_t len = buf2.len(); // char shortaddr[8]; // // while (len - i >= 2) { // uint8_t attrid = buf2.get8(i++); // // snprintf_P(shortaddr, sizeof(shortaddr), PSTR("0x%02X"), attrid); // // //json[shortaddr] = parseSingleAttribute(json_lumi, buf2, i, len, nullptr, 0); // } // // parse output // if (json_lumi.containsKey("0x64")) { // Temperature // int32_t temperature = json_lumi["0x64"]; // json[F(D_JSON_TEMPERATURE)] = temperature / 100.0f; // } // if (json_lumi.containsKey("0x65")) { // Humidity // uint32_t humidity = json_lumi["0x65"]; // json[F(D_JSON_HUMIDITY)] = humidity / 100.0f; // } // if (json_lumi.containsKey("0x66")) { // Pressure // int32_t pressure = json_lumi["0x66"]; // json[F(D_JSON_PRESSURE)] = pressure / 100.0f; // json[F(D_JSON_PRESSURE_UNIT)] = F(D_UNIT_PRESSURE); // hPa // } // if (json_lumi.containsKey("0x01")) { // Battery Voltage // uint32_t voltage = json_lumi["0x01"]; // json[F(D_JSON_VOLTAGE)] = voltage / 1000.0f; // json[F("Battery")] = toPercentageCR2032(voltage); // } // json.remove(key); // } // // } #endif // USE_ZIGBEE